Commit 03bcd811 authored by James Lopez's avatar James Lopez

Merge branch...

Merge branch '254182-fix-notificationevent-namespace-issue-on-custom-notification-setting-popup' into 'master'

Fix NotificationEvent namespace issue on Notification Setting

See merge request gitlab-org/gitlab!48736
parents 48707d21 4527bca5
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
RUBY_GC_MALLOC_LIMIT_MAX: 134217728 RUBY_GC_MALLOC_LIMIT_MAX: 134217728
CRYSTALBALL: "true" CRYSTALBALL: "true"
RECORD_DEPRECATIONS: "true" RECORD_DEPRECATIONS: "true"
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets"] needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets", "detect-tests"]
script: script:
- *base-script - *base-script
- rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag ~level:migration" - rspec_paralellized_job "--tag ~quarantine --tag ~geo --tag ~level:migration"
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
- .rspec-base - .rspec-base
- .as-if-foss - .as-if-foss
- .use-pg11 - .use-pg11
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets as-if-foss"] needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets as-if-foss", "detect-tests"]
.rspec-ee-base-pg11: .rspec-ee-base-pg11:
extends: extends:
......
...@@ -61,14 +61,16 @@ verify-tests-yml: ...@@ -61,14 +61,16 @@ verify-tests-yml:
- scripts/verify-tff-mapping - scripts/verify-tff-mapping
.detect-test-base: .detect-test-base:
image: ruby:2.7-alpine image: ruby:2.7
needs: [] needs: []
stage: prepare stage: prepare
script: script:
- source scripts/utils.sh - source ./scripts/utils.sh
- source ./scripts/rspec_helpers.sh
- install_gitlab_gem - install_gitlab_gem
- install_tff_gem - install_tff_gem
- tooling/bin/find_foss_tests ${MATCHED_TESTS_FILE} - retrieve_tests_mapping
- tooling/bin/find_tests ${MATCHED_TESTS_FILE}
- 'echo "test files affected: $(cat $MATCHED_TESTS_FILE)"' - 'echo "test files affected: $(cat $MATCHED_TESTS_FILE)"'
artifacts: artifacts:
expire_in: 7d expire_in: 7d
......
cb5c003584e180415fe33d1712e0d5aee2a22b0c a5a5d83630f13c3eb3e1650a24423fc5e9bc47d2
<svg width="234" height="162" viewBox="0 0 234 162" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M174.68 56.344H200.5C215.412 56.344 227.5 44.1787 227.5 29.172C227.5 14.1653 215.412 2 200.5 2C185.588 2 173.5 14.1653 173.5 29.172C173.5 36.2548 176.193 42.7046 180.604 47.5412" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
<path d="M145.5 76.4714C145.5 65.3553 154.454 56.344 165.5 56.344" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
<path d="M102.5 121.758H29.5C14.5883 121.758 2.5 109.593 2.5 94.586C2.5 79.5794 14.5883 67.4141 29.5 67.4141C44.4117 67.4141 56.5 79.5794 56.5 94.586C56.5 101.669 53.8072 108.119 49.3957 112.955" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
<path d="M67.0466 121.758H52.5C42.5589 121.758 34.5 129.868 34.5 139.873C34.5 149.877 42.5589 157.987 52.5 157.987C62.4411 157.987 70.5 149.877 70.5 139.873C70.5 137.478 70.0384 135.192 69.1998 133.1" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
<g clip-path="url(#clip0)">
<path d="M55.0188 135.3C55.1617 134.764 54.8451 134.211 54.3117 134.068C53.7782 133.925 53.2298 134.243 53.0869 134.78L49.9811 146.445C49.8381 146.981 50.1547 147.534 50.6882 147.677C51.2217 147.821 51.77 147.503 51.9129 146.965L55.0188 135.3Z" fill="#FC6D26"/>
<path d="M49.2071 137.142C49.5976 137.534 49.5976 138.172 49.2071 138.565L46.9142 140.873L49.2071 143.18C49.5976 143.573 49.5976 144.211 49.2071 144.603C48.8166 144.997 48.1834 144.997 47.7929 144.603L44.7929 141.584C44.4024 141.192 44.4024 140.554 44.7929 140.161L47.7929 137.142C48.1834 136.748 48.8166 136.748 49.2071 137.142Z" fill="#FC6D26"/>
<path d="M55.7929 137.142C55.4024 137.534 55.4024 138.172 55.7929 138.565L58.0858 140.873L55.7929 143.18C55.4024 143.573 55.4024 144.211 55.7929 144.603C56.1834 144.997 56.8166 144.997 57.2071 144.603L60.2071 141.584C60.5976 141.192 60.5976 140.554 60.2071 140.161L57.2071 137.142C56.8166 136.748 56.1834 136.748 55.7929 137.142Z" fill="#FC6D26"/>
</g>
<path d="M212.102 160C222.815 160 231.5 151.214 231.5 140.376C231.5 129.537 222.815 120.752 212.102 120.752H151.5" stroke="#C2B7E6" stroke-width="4" stroke-linecap="round"/>
<path d="M126.5 138.866C107.171 138.866 91.5 123.096 91.5 103.643C91.5 84.191 107.171 68.4204 126.5 68.4204C145.829 68.4204 161.5 84.191 161.5 103.643C161.5 123.096 145.829 138.866 126.5 138.866ZM126.5 131.451C141.76 131.451 154.132 119.001 154.132 103.643C154.132 88.2861 141.76 75.8358 126.5 75.8358C111.24 75.8358 98.8684 88.2861 98.8684 103.643C98.8684 119.001 111.24 131.451 126.5 131.451Z" fill="#FC6D26"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M126.126 87.1326C135.355 87.1326 142.906 94.5624 142.906 103.643C142.906 112.724 135.355 120.154 126.126 120.154C120.672 120.154 115.638 117.265 112.281 113.137L126.126 103.643V87.1326Z" fill="#6E49CB"/>
<g clip-path="url(#clip1)">
<path d="M29.5 90.2659L24.3571 91.9534V93.1629C24.3571 94.9623 25.087 96.6872 26.3846 97.9546L29.5 100.997V90.2659Z" fill="#FC6D26"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5 86.8909L29.5 83.5159L41.5 86.8909V93.1115C41.5 96.6919 40.0551 100.126 37.4832 102.657L29.5 110.516L21.5168 102.657C18.9449 100.126 17.5 96.6919 17.5 93.1115V86.8909ZM20.9286 93.1115V89.4366L29.5 87.0259L38.0714 89.4366V93.1115C38.0714 95.7968 36.9878 98.3721 35.0588 100.271L29.5 105.743L23.9412 100.271C22.0122 98.3721 20.9286 95.7968 20.9286 93.1115Z" fill="#FC6D26"/>
</g>
<g clip-path="url(#clip2)">
<path d="M210.857 19.7297L209.51 24.8237C208.922 27.0445 207.518 28.9576 205.581 30.1752L194.728 36.999L191.862 34.1146L198.642 23.1922C199.852 21.2431 201.753 19.8298 203.96 19.2386L209.022 17.8826C209.822 17.6681 210.644 18.1474 210.857 18.953C210.925 19.2075 210.925 19.4752 210.857 19.7297ZM207.292 21.4702L204.732 22.1561C203.261 22.5503 201.993 23.4925 201.187 24.7918L196.517 32.3146L203.992 27.6148C205.283 26.803 206.219 25.5276 206.611 24.0471L207.292 21.4702ZM196.5 38.2294L204 33.7007V35.2103C204 38.5451 201.314 41.2485 198 41.2485H196.5V38.2294ZM190.5 32.1912H187.5V30.6816C187.5 27.3468 190.186 24.6434 193.5 24.6434H195L190.5 32.1912Z" fill="#FC6D26"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M209.914 132.822C209.384 132.822 208.875 133.032 208.5 133.407L204.796 137.111C204.613 137.293 204.5 137.544 204.5 137.822V144.822C204.5 145.926 205.395 146.822 206.5 146.822H216.5C217.605 146.822 218.5 145.926 218.5 144.822V137.822C218.5 137.546 218.388 137.296 218.207 137.115L214.5 133.407C214.125 133.032 213.616 132.822 213.086 132.822H209.914ZM215.086 136.822L213.086 134.822H212.5V136.822H215.086ZM210.5 134.822H209.914L207.914 136.822H210.5V134.822ZM206.5 138.822H216.5V144.822H206.5V138.822Z" fill="#FC6D26"/>
<defs>
<clipPath id="clip0">
<rect width="16" height="13.6779" fill="white" transform="translate(44.5 134.033)"/>
</clipPath>
<clipPath id="clip1">
<rect width="24" height="27.172" fill="white" transform="translate(17.5 83.5159)"/>
</clipPath>
<clipPath id="clip2">
<rect width="24" height="24.1529" fill="white" transform="translate(187.5 17.0956)"/>
</clipPath>
</defs>
</svg>
<script> <script>
import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex'; import { createNamespacedHelpers, mapState, mapActions, mapGetters } from 'vuex';
import { GlFormGroup, GlFormInput, GlFormCheckbox, GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; import {
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlIcon,
GlLink,
GlSprintf,
GlButton,
} from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue'; import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
import { KUBERNETES_VERSIONS } from '../constants'; import { KUBERNETES_VERSIONS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
const { mapState: mapRolesState, mapActions: mapRolesActions } = createNamespacedHelpers('roles'); const { mapState: mapRolesState, mapActions: mapRolesActions } = createNamespacedHelpers('roles');
const { mapState: mapKeyPairsState, mapActions: mapKeyPairsActions } = createNamespacedHelpers( const { mapState: mapKeyPairsState, mapActions: mapKeyPairsActions } = createNamespacedHelpers(
...@@ -29,7 +36,7 @@ export default { ...@@ -29,7 +36,7 @@ export default {
GlIcon, GlIcon,
GlLink, GlLink,
GlSprintf, GlSprintf,
LoadingButton, GlButton,
}, },
props: { props: {
gitlabManagedClusterHelpPath: { gitlabManagedClusterHelpPath: {
...@@ -508,13 +515,16 @@ export default { ...@@ -508,13 +515,16 @@ export default {
</p> </p>
</div> </div>
<div class="form-group"> <div class="form-group">
<loading-button <gl-button
class="js-create-cluster btn-success" variant="success"
category="primary"
class="js-create-cluster"
:disabled="createClusterButtonDisabled" :disabled="createClusterButtonDisabled"
:loading="isCreatingCluster" :loading="isCreatingCluster"
:label="createClusterButtonLabel"
@click="createCluster()" @click="createCluster()"
/> >
{{ createClusterButtonLabel }}
</gl-button>
</div> </div>
</form> </form>
</template> </template>
...@@ -10,19 +10,20 @@ const initJiraFormHandlers = () => { ...@@ -10,19 +10,20 @@ const initJiraFormHandlers = () => {
AP.navigator.reload(); AP.navigator.reload();
}; };
const reqFailed = res => { const reqFailed = (res, fallbackErrorMessage) => {
const { responseJSON: { error } = {} } = res || {};
// eslint-disable-next-line no-alert // eslint-disable-next-line no-alert
alert(res.responseJSON.error); alert(error || fallbackErrorMessage);
}; };
AP.getLocation(location => { AP.getLocation(location => {
$('.js-jira-connect-sign-in').each(() => { $('.js-jira-connect-sign-in').each(function updateSignInLink() {
const updatedLink = `${$(this).attr('href')}?return_to=${location}`; const updatedLink = `${$(this).attr('href')}?return_to=${location}`;
$(this).attr('href', updatedLink); $(this).attr('href', updatedLink);
}); });
}); });
$('#add-subscription-form').on('submit', e => { $('#add-subscription-form').on('submit', function onAddSubscriptionForm(e) {
const actionUrl = $(this).attr('action'); const actionUrl = $(this).attr('action');
e.preventDefault(); e.preventDefault();
...@@ -34,11 +35,11 @@ const initJiraFormHandlers = () => { ...@@ -34,11 +35,11 @@ const initJiraFormHandlers = () => {
format: 'json', format: 'json',
}) })
.done(reqComplete) .done(reqComplete)
.fail(reqFailed); .fail(err => reqFailed(err, 'Failed to add namespace. Please try again.'));
}); });
}); });
$('.remove-subscription').on('click', e => { $('.remove-subscription').on('click', function onRemoveSubscriptionClick(e) {
const href = $(this).attr('href'); const href = $(this).attr('href');
e.preventDefault(); e.preventDefault();
...@@ -53,7 +54,7 @@ const initJiraFormHandlers = () => { ...@@ -53,7 +54,7 @@ const initJiraFormHandlers = () => {
}, },
}) })
.done(reqComplete) .done(reqComplete)
.fail(reqFailed); .fail(err => reqFailed(err, 'Failed to remove namespace. Please try again.'));
}); });
}); });
}; };
......
...@@ -85,6 +85,7 @@ export default { ...@@ -85,6 +85,7 @@ export default {
v-model="filter" v-model="filter"
:placeholder="$options.i18n.searchPlaceholder" :placeholder="$options.i18n.searchPlaceholder"
class="gl-align-self-center gl-ml-auto fork-filtered-search" class="gl-align-self-center gl-ml-auto fork-filtered-search"
data-qa-selector="fork_groups_list_search_field"
/> />
</template> </template>
</gl-tabs> </gl-tabs>
......
import Vue from 'vue'; import Vue from 'vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
import Tracking from '~/tracking';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const remainingTimeElements = document.querySelectorAll('.js-remaining-time'); const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
...@@ -13,4 +14,13 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -13,4 +14,13 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
}), }),
); );
const trackButtonClick = () => {
if (gon.tracking_data) {
const { category, action, ...data } = gon.tracking_data;
Tracking.event(category, action, data);
}
};
const buttons = document.querySelectorAll('.js-empty-state-button');
buttons.forEach(button => button.addEventListener('click', trackButtonClick));
}); });
<script> <script>
import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
export default { export default {
components: {
GlIcon,
},
props: { props: {
user: { user: {
type: Object, type: Object,
...@@ -46,6 +50,6 @@ export default { ...@@ -46,6 +50,6 @@ export default {
class="avatar avatar-inline m-0" class="avatar avatar-inline m-0"
data-qa-selector="avatar_image" data-qa-selector="avatar_image"
/> />
<i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i> <gl-icon v-if="hasMergeIcon" name="warning-solid" aria-hidden="true" class="merge-icon" />
</span> </span>
</template> </template>
...@@ -112,11 +112,12 @@ export default { ...@@ -112,11 +112,12 @@ export default {
/> />
<button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button"> <button v-if="hasMoreThanTwoAssignees" class="btn-link" type="button">
<span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span> <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
<i <gl-icon
v-if="isMergeRequest && !allAssigneesCanMerge" v-if="isMergeRequest && !allAssigneesCanMerge"
name="warning-solid"
aria-hidden="true" aria-hidden="true"
class="fa fa-exclamation-triangle merge-icon" class="merge-icon"
></i> />
</button> </button>
</div> </div>
</template> </template>
...@@ -97,11 +97,12 @@ export default { ...@@ -97,11 +97,12 @@ export default {
<collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" /> <collapsed-reviewer v-for="user in collapsedUsers" :key="user.id" :user="user" />
<button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button"> <button v-if="hasMoreThanTwoReviewers" class="btn-link" type="button">
<span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span> <span class="avatar-counter sidebar-avatar-counter"> {{ sidebarAvatarCounter }} </span>
<i <gl-icon
v-if="!allReviewersCanMerge" v-if="!allReviewersCanMerge"
name="warning-solid"
aria-hidden="true" aria-hidden="true"
class="fa fa-exclamation-triangle merge-icon" class="merge-icon"
></i> />
</button> </button>
</div> </div>
</template> </template>
<script> <script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees // NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 // It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
export default { export default {
components: {
GlIcon,
},
props: { props: {
user: { user: {
type: Object, type: Object,
...@@ -38,6 +42,6 @@ export default { ...@@ -38,6 +42,6 @@ export default {
class="avatar avatar-inline m-0" class="avatar avatar-inline m-0"
data-qa-selector="avatar_image" data-qa-selector="avatar_image"
/> />
<i v-if="hasMergeIcon" aria-hidden="true" class="fa fa-exclamation-triangle merge-icon"></i> <gl-icon v-if="hasMergeIcon" name="warning-solid" aria-hidden="true" class="merge-icon" />
</span> </span>
</template> </template>
...@@ -796,7 +796,7 @@ UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) { ...@@ -796,7 +796,7 @@ UsersSelect.prototype.renderRowAvatar = function(issuableType, user, img) {
const mergeIcon = const mergeIcon =
issuableType === 'merge_request' && !user.can_merge issuableType === 'merge_request' && !user.can_merge
? `${spriteIcon('warning-solid', 's12 merge-icon')}` ? spriteIcon('warning-solid', 's12 merge-icon')
: ''; : '';
return `<span class="position-relative mr-2"> return `<span class="position-relative mr-2">
......
...@@ -113,7 +113,7 @@ ...@@ -113,7 +113,7 @@
position: absolute; position: absolute;
bottom: 0; bottom: 0;
right: 0; right: 0;
text-shadow: -1px -1px 2px $white, 1px -1px 2px $white, -1px 1px 2px $white, 1px 1px 2px $white; filter: drop-shadow(0 0 0.5px $white) drop-shadow(0 0 1px $white) drop-shadow(0 0 2px $white);
} }
} }
...@@ -392,6 +392,13 @@ ...@@ -392,6 +392,13 @@
text-align: center; text-align: center;
} }
.merge-icon {
height: 12px;
width: 12px;
bottom: -5px;
right: 4px;
}
.sidebar-collapsed-icon { .sidebar-collapsed-icon {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -402,7 +409,7 @@ ...@@ -402,7 +409,7 @@
text-align: center; text-align: center;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
svg { > svg {
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
} }
...@@ -410,7 +417,7 @@ ...@@ -410,7 +417,7 @@
&:hover .todo-undone { &:hover .todo-undone {
color: $gl-text-color; color: $gl-text-color;
svg { > svg {
fill: $gl-text-color; fill: $gl-text-color;
} }
} }
...@@ -482,10 +489,6 @@ ...@@ -482,10 +489,6 @@
display: none; display: none;
} }
.merge-icon {
font-size: 10px;
}
.multiple-users { .multiple-users {
position: relative; position: relative;
height: 24px; height: 24px;
......
...@@ -287,10 +287,6 @@ $mr-widget-min-height: 69px; ...@@ -287,10 +287,6 @@ $mr-widget-min-height: 69px;
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
&.has-conflicts .fa-exclamation-triangle {
color: $orange-500;
}
time { time {
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
} }
......
...@@ -17,6 +17,9 @@ class Projects::JobsController < Projects::ApplicationController ...@@ -17,6 +17,9 @@ class Projects::JobsController < Projects::ApplicationController
before_action do before_action do
push_frontend_feature_flag(:ci_job_line_links, @project) push_frontend_feature_flag(:ci_job_line_links, @project)
end end
before_action only: :index do
frontend_experimentation_tracking_data(:jobs_empty_state, 'click_button')
end
layout 'project' layout 'project'
......
...@@ -13,6 +13,7 @@ module IconsHelper ...@@ -13,6 +13,7 @@ module IconsHelper
# Right now this method simply delegates directly to `fa_icon` from the # Right now this method simply delegates directly to `fa_icon` from the
# font-awesome-rails gem, but should we ever use a different icon pack in the # font-awesome-rails gem, but should we ever use a different icon pack in the
# future we won't have to change hundreds of method calls. # future we won't have to change hundreds of method calls.
# @deprecated use sprite_icon to render a SVG icon
def icon(names, options = {}) def icon(names, options = {})
if (options.keys & %w[aria-hidden aria-label data-hidden]).empty? if (options.keys & %w[aria-hidden aria-label data-hidden]).empty?
# Add 'aria-hidden' and 'data-hidden' if they are not set in options. # Add 'aria-hidden' and 'data-hidden' if they are not set in options.
......
...@@ -13,6 +13,8 @@ class AuditEvent < ApplicationRecord ...@@ -13,6 +13,8 @@ class AuditEvent < ApplicationRecord
:target_id :target_id
].freeze ].freeze
self.primary_key = :id
serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize serialize :details, Hash # rubocop:disable Cop/ActiveRecordSerialize
belongs_to :user, foreign_key: :author_id belongs_to :user, foreign_key: :author_id
......
# frozen_string_literal: true
class DependencyProxy::Manifest < ApplicationRecord
include FileStoreMounter
belongs_to :group
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
validates :digest, presence: true
mount_file_store_uploader DependencyProxy::FileUploader
end
...@@ -21,7 +21,7 @@ module ExclusiveLeaseGuard ...@@ -21,7 +21,7 @@ module ExclusiveLeaseGuard
lease = exclusive_lease.try_obtain lease = exclusive_lease.try_obtain
unless lease unless lease
log_error("Cannot obtain an exclusive lease for #{self.class.name}. There must be another instance already in execution.") log_error("Cannot obtain an exclusive lease for #{lease_key}. There must be another instance already in execution.")
return return
end end
......
# frozen_string_literal: true
module Pages
module LegacyStorageLease
extend ActiveSupport::Concern
include ::ExclusiveLeaseGuard
LEASE_TIMEOUT = 1.hour
# override method from exclusive lease guard to guard it by feature flag
# TODO: just remove this method after testing this in production
# https://gitlab.com/gitlab-org/gitlab/-/issues/282464
def try_obtain_lease
return yield unless Feature.enabled?(:pages_use_legacy_storage_lease, project)
super
end
def lease_key
"pages_legacy_storage:#{project.id}"
end
def lease_timeout
LEASE_TIMEOUT
end
end
end
...@@ -4,6 +4,9 @@ module Projects ...@@ -4,6 +4,9 @@ module Projects
class UpdatePagesService < BaseService class UpdatePagesService < BaseService
InvalidStateError = Class.new(StandardError) InvalidStateError = Class.new(StandardError)
FailedToExtractError = Class.new(StandardError) FailedToExtractError = Class.new(StandardError)
ExclusiveLeaseTaken = Class.new(StandardError)
include ::Pages::LegacyStorageLease
BLOCK_SIZE = 32.kilobytes BLOCK_SIZE = 32.kilobytes
PUBLIC_DIR = 'public' PUBLIC_DIR = 'public'
...@@ -109,6 +112,17 @@ module Projects ...@@ -109,6 +112,17 @@ module Projects
end end
def deploy_page!(archive_public_path) def deploy_page!(archive_public_path)
deployed = try_obtain_lease do
deploy_page_unsafe!(archive_public_path)
true
end
unless deployed
raise ExclusiveLeaseTaken, "Failed to deploy pages - other deployment is in progress"
end
end
def deploy_page_unsafe!(archive_public_path)
# Do atomic move of pages # Do atomic move of pages
# Move and removal may not be atomic, but they are significantly faster then extracting and removal # Move and removal may not be atomic, but they are significantly faster then extracting and removal
# 1. We move deployed public to previous public path (file removal is slow) # 1. We move deployed public to previous public path (file removal is slow)
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
- if current_user.admin? - if current_user.admin?
.text-warning .text-warning
%p %p
= icon("exclamation-triangle fw") = sprite_icon('warning-solid')
= html_escape(_('You are an admin, which means granting access to %{client_name} will allow them to interact with GitLab as an admin as well. Proceed with caution.')) % { client_name: tag.strong(@pre_auth.client.name) } = html_escape(_('You are an admin, which means granting access to %{client_name} will allow them to interact with GitLab as an admin as well. Proceed with caution.')) % { client_name: tag.strong(@pre_auth.client.name) }
%p %p
- link_to_client = link_to(@pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer') - link_to_client = link_to(@pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer')
......
- if project.archived? - if project.archived?
.text-warning.center.prepend-top-20 .text-warning.center.prepend-top-20
%p %p
= icon("exclamation-triangle fw") = sprite_icon('warning-solid')
= _('Archived project! Repository and other project resources are read only') = _('Archived project! Repository and other project resources are read only')
...@@ -37,7 +37,9 @@ ...@@ -37,7 +37,9 @@
= _('Add previously merged commits') = _('Add previously merged commits')
- if commits.size == 0 && context_commits.nil? - if commits.size == 0 && context_commits.nil?
.mt-4.text-center .commits-empty.gl-mt-6
.bold = custom_icon('illustration_no_commits')
%h4
= _('Your search didn\'t match any commits.') = _('Your search didn\'t match any commits.')
= _('Try changing or removing filters.') %p
= _('Try changing or removing filters.')
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
.control .control
= form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do
= search_field_tag :search, params[:search], { placeholder: _('Search by message'), id: 'commits-search', class: 'form-control search-text-input input-short gl-mt-3 gl-sm-mt-0 gl-min-w-full', spellcheck: false } = search_field_tag :search, params[:search], { placeholder: _('Search by message'), id: 'commits-search', class: 'form-control gl-form-input input-short gl-mt-3 gl-sm-mt-0 gl-min-w-full gl-inset-border-1-gray-200!', spellcheck: false }
.control.d-none.d-md-block .control.d-none.d-md-block
= link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn gl-button btn-svg' do = link_to project_commits_path(@project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn gl-button btn-svg' do
= sprite_icon('rss', css_class: 'qa-rss-icon') = sprite_icon('rss', css_class: 'qa-rss-icon')
......
- admin = local_assigns.fetch(:admin, false) - admin = local_assigns.fetch(:admin, false)
- if builds.blank? - if builds.blank?
%div - if experiment_enabled?(:jobs_empty_state)
.nothing-here-block No jobs to show .row.empty-state
.col-12
.svg-content.svg-250
= image_tag('jobs-empty-state.svg')
.col-12
.text-content.gl-text-center
%h4
= s_('Jobs|Use jobs to automate your tasks')
%p
= s_('Jobs|Jobs are the building blocks of a GitLab CI/CD pipeline. Each job has a specific task, like testing code. To set up jobs in a CI/CD pipeline, add a CI/CD configuration file to your project.')
= link_to s_('Jobs|Create CI/CD configuration file'), help_page_path('ci/quick_start/README'), class: 'btn gl-button btn-info js-empty-state-button'
- else
.nothing-here-block= s_('Jobs|No jobs to show')
- else - else
.table-holder .table-holder
%table.table.ci-table.builds-page %table.table.ci-table.builds-page
......
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
.nav-controls .nav-controls
- if can?(current_user, :update_build, @project) - if can?(current_user, :update_build, @project)
- unless @repository.gitlab_ci_yml - if !@repository.gitlab_ci_yml && !experiment_enabled?(:jobs_empty_state)
= link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn gl-button btn-info' = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn gl-button btn-info js-empty-state-button'
= link_to project_ci_lint_path(@project), class: 'btn gl-button btn-default' do = link_to project_ci_lint_path(@project), class: 'btn gl-button btn-default' do
%span CI lint %span CI lint
......
%h4 %h4
= icon('exclamation-triangle') = sprite_icon('warning-solid')
This merge request failed to be merged automatically This merge request failed to be merged automatically
%p %p
......
%tr.tree-truncated-warning %tr.tree-truncated-warning
%td{ colspan: '3' } %td{ colspan: '3' }
= icon('exclamation-triangle fw') = sprite_icon('warning-solid')
%span %span
Too many items to show. To preserve performance only Too many items to show. To preserve performance only
%strong #{number_with_delimiter(limit)} of #{number_with_delimiter(total)} %strong #{number_with_delimiter(limit)} of #{number_with_delimiter(total)}
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
- if referenced_users - if referenced_users
.referenced-users.hide .referenced-users.hide
%span %span
= icon("exclamation-triangle") = sprite_icon('warning-solid')
You are about to add You are about to add
%strong %strong
%span.js-referenced-users-count 0 %span.js-referenced-users-count 0
......
...@@ -2021,7 +2021,7 @@ ...@@ -2021,7 +2021,7 @@
:urgency: :low :urgency: :low
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
:idempotent: :idempotent: true
:tags: [] :tags: []
- :name: self_monitoring_project_create - :name: self_monitoring_project_create
:feature_category: :metrics :feature_category: :metrics
......
# frozen_string_literal: true # frozen_string_literal: true
class RepositoryUpdateRemoteMirrorWorker # rubocop:disable Scalability/IdempotentWorker class RepositoryUpdateRemoteMirrorWorker
UpdateError = Class.new(StandardError) UpdateError = Class.new(StandardError)
include ApplicationWorker include ApplicationWorker
...@@ -11,6 +11,7 @@ class RepositoryUpdateRemoteMirrorWorker # rubocop:disable Scalability/Idempoten ...@@ -11,6 +11,7 @@ class RepositoryUpdateRemoteMirrorWorker # rubocop:disable Scalability/Idempoten
sidekiq_options retry: 3, dead: false sidekiq_options retry: 3, dead: false
feature_category :source_code_management feature_category :source_code_management
loggable_arguments 1 loggable_arguments 1
idempotent!
LOCK_WAIT_TIME = 30.seconds LOCK_WAIT_TIME = 30.seconds
MAX_TRIES = 3 MAX_TRIES = 3
......
---
title: Update empty state for no commits result
merge_request: 48538
author:
type: changed
---
title: Fix misalignment of commit search by message input
merge_request: 48430
author:
type: fixed
---
title: Add dependency_proxy_manifests table and associations
merge_request: 48535
author:
type: added
---
title: Replace fa-exclamation-triangle icons with GitLab SVG warning-solid icon
merge_request: 47089
author:
type: changed
---
title: Resolve Cannot remove namespace
merge_request: 48973
author:
type: fixed
--- ---
name: branch_list_keyset_pagination name: branch_list_keyset_pagination
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35819 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35819
rollout_issue_url: rollout_issue_url:
milestone: '13.2' milestone: '13.2'
type: development type: development
group: group::source code group: group::source code
default_enabled: false default_enabled: true
---
name: pages_use_legacy_storage_lease
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48349
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/282464
milestone: '13.7'
type: development
group: group::release
default_enabled: false
# frozen_string_literal: true
class CreateDependencyProxyManifests < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
with_lock_retries do
create_table :dependency_proxy_manifests, if_not_exists: true do |t|
t.timestamps_with_timezone
t.references :group, index: false, null: false, foreign_key: { to_table: :namespaces, on_delete: :cascade }, type: :bigint
t.bigint :size
t.integer :file_store, limit: 2
t.text :file_name, null: false
t.text :file, null: false
t.text :digest, null: false
t.index [:group_id, :digest], name: 'index_dependency_proxy_manifests_on_group_id_and_digest'
end
end
add_text_limit :dependency_proxy_manifests, :file_name, 255
add_text_limit :dependency_proxy_manifests, :file, 255
add_text_limit :dependency_proxy_manifests, :digest, 255
end
def down
drop_table :dependency_proxy_manifests
end
end
e19c6d019f1478e5998b2a264c5327dc82da7fde7edd19b15da70a30c5779844
\ No newline at end of file
...@@ -11617,6 +11617,30 @@ CREATE SEQUENCE dependency_proxy_group_settings_id_seq ...@@ -11617,6 +11617,30 @@ CREATE SEQUENCE dependency_proxy_group_settings_id_seq
ALTER SEQUENCE dependency_proxy_group_settings_id_seq OWNED BY dependency_proxy_group_settings.id; ALTER SEQUENCE dependency_proxy_group_settings_id_seq OWNED BY dependency_proxy_group_settings.id;
CREATE TABLE dependency_proxy_manifests (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
group_id bigint NOT NULL,
size bigint,
file_store smallint,
file_name text NOT NULL,
file text NOT NULL,
digest text NOT NULL,
CONSTRAINT check_079b293a7b CHECK ((char_length(file) <= 255)),
CONSTRAINT check_c579e3f586 CHECK ((char_length(file_name) <= 255)),
CONSTRAINT check_f5d9996bf1 CHECK ((char_length(digest) <= 255))
);
CREATE SEQUENCE dependency_proxy_manifests_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE dependency_proxy_manifests_id_seq OWNED BY dependency_proxy_manifests.id;
CREATE TABLE deploy_keys_projects ( CREATE TABLE deploy_keys_projects (
id integer NOT NULL, id integer NOT NULL,
deploy_key_id integer NOT NULL, deploy_key_id integer NOT NULL,
...@@ -18014,6 +18038,8 @@ ALTER TABLE ONLY dependency_proxy_blobs ALTER COLUMN id SET DEFAULT nextval('dep ...@@ -18014,6 +18038,8 @@ ALTER TABLE ONLY dependency_proxy_blobs ALTER COLUMN id SET DEFAULT nextval('dep
ALTER TABLE ONLY dependency_proxy_group_settings ALTER COLUMN id SET DEFAULT nextval('dependency_proxy_group_settings_id_seq'::regclass); ALTER TABLE ONLY dependency_proxy_group_settings ALTER COLUMN id SET DEFAULT nextval('dependency_proxy_group_settings_id_seq'::regclass);
ALTER TABLE ONLY dependency_proxy_manifests ALTER COLUMN id SET DEFAULT nextval('dependency_proxy_manifests_id_seq'::regclass);
ALTER TABLE ONLY deploy_keys_projects ALTER COLUMN id SET DEFAULT nextval('deploy_keys_projects_id_seq'::regclass); ALTER TABLE ONLY deploy_keys_projects ALTER COLUMN id SET DEFAULT nextval('deploy_keys_projects_id_seq'::regclass);
ALTER TABLE ONLY deploy_tokens ALTER COLUMN id SET DEFAULT nextval('deploy_tokens_id_seq'::regclass); ALTER TABLE ONLY deploy_tokens ALTER COLUMN id SET DEFAULT nextval('deploy_tokens_id_seq'::regclass);
...@@ -19136,6 +19162,9 @@ ALTER TABLE ONLY dependency_proxy_blobs ...@@ -19136,6 +19162,9 @@ ALTER TABLE ONLY dependency_proxy_blobs
ALTER TABLE ONLY dependency_proxy_group_settings ALTER TABLE ONLY dependency_proxy_group_settings
ADD CONSTRAINT dependency_proxy_group_settings_pkey PRIMARY KEY (id); ADD CONSTRAINT dependency_proxy_group_settings_pkey PRIMARY KEY (id);
ALTER TABLE ONLY dependency_proxy_manifests
ADD CONSTRAINT dependency_proxy_manifests_pkey PRIMARY KEY (id);
ALTER TABLE ONLY deploy_keys_projects ALTER TABLE ONLY deploy_keys_projects
ADD CONSTRAINT deploy_keys_projects_pkey PRIMARY KEY (id); ADD CONSTRAINT deploy_keys_projects_pkey PRIMARY KEY (id);
...@@ -20876,6 +20905,8 @@ CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON dependenc ...@@ -20876,6 +20905,8 @@ CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON dependenc
CREATE INDEX index_dependency_proxy_group_settings_on_group_id ON dependency_proxy_group_settings USING btree (group_id); CREATE INDEX index_dependency_proxy_group_settings_on_group_id ON dependency_proxy_group_settings USING btree (group_id);
CREATE INDEX index_dependency_proxy_manifests_on_group_id_and_digest ON dependency_proxy_manifests USING btree (group_id, digest);
CREATE INDEX index_deploy_key_id_on_protected_branch_push_access_levels ON protected_branch_push_access_levels USING btree (deploy_key_id); CREATE INDEX index_deploy_key_id_on_protected_branch_push_access_levels ON protected_branch_push_access_levels USING btree (deploy_key_id);
CREATE INDEX index_deploy_keys_projects_on_deploy_key_id ON deploy_keys_projects USING btree (deploy_key_id); CREATE INDEX index_deploy_keys_projects_on_deploy_key_id ON deploy_keys_projects USING btree (deploy_key_id);
...@@ -24474,6 +24505,9 @@ ALTER TABLE ONLY user_permission_export_uploads ...@@ -24474,6 +24505,9 @@ ALTER TABLE ONLY user_permission_export_uploads
ALTER TABLE ONLY repository_languages ALTER TABLE ONLY repository_languages
ADD CONSTRAINT fk_rails_a750ec87a8 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_a750ec87a8 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY dependency_proxy_manifests
ADD CONSTRAINT fk_rails_a758021fb0 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY resource_milestone_events ALTER TABLE ONLY resource_milestone_events
ADD CONSTRAINT fk_rails_a788026e85 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_a788026e85 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
......
...@@ -41,6 +41,7 @@ exceptions: ...@@ -41,6 +41,7 @@ exceptions:
- GCP - GCP
- GDK - GDK
- GET - GET
- GIF
- GKE - GKE
- GNU - GNU
- GPG - GPG
...@@ -56,6 +57,8 @@ exceptions: ...@@ -56,6 +57,8 @@ exceptions:
- IMAP - IMAP
- IRC - IRC
- ISO - ISO
- JPEG
- JPG
- JSON - JSON
- LAN - LAN
- LDAP - LDAP
...@@ -76,6 +79,7 @@ exceptions: ...@@ -76,6 +79,7 @@ exceptions:
- PDF - PDF
- PGP - PGP
- PHP - PHP
- PNG
- POST - POST
- PUT - PUT
- RAM - RAM
...@@ -92,6 +96,7 @@ exceptions: ...@@ -92,6 +96,7 @@ exceptions:
- SCP - SCP
- SCSS - SCSS
- SDK - SDK
- SEO
- SHA - SHA
- SLA - SLA
- SMTP - SMTP
......
...@@ -120,6 +120,7 @@ dequarantining ...@@ -120,6 +120,7 @@ dequarantining
DevOps DevOps
discoverability discoverability
Disqus Disqus
Divio
Dockerfile Dockerfile
Dockerfiles Dockerfiles
dogfood dogfood
...@@ -344,6 +345,7 @@ prepend ...@@ -344,6 +345,7 @@ prepend
prepended prepended
prepending prepending
prepends prepends
Prettifier
Pritaly Pritaly
profiler profiler
Prometheus Prometheus
......
...@@ -143,16 +143,39 @@ jq 'select(."grpc.code" != null and ."grpc.code" != "OK")' current ...@@ -143,16 +143,39 @@ jq 'select(."grpc.code" != null and ."grpc.code" != "OK")' current
jq 'select(."grpc.time_ms" > 30000)' current jq 'select(."grpc.time_ms" > 30000)' current
``` ```
#### Print top three projects by request volume and their three longest durations #### Print top ten projects by request volume and their three longest durations
```shell ```shell
jq -s -r 'map(select(."grpc.request.glProjectPath" != null and ."grpc.request.glProjectPath" != "" and ."grpc.time_ms" != null)) | group_by(."grpc.request.glProjectPath") | sort_by(-length) | limit(3; .[]) | sort_by(-."grpc.time_ms") | "CT: \(length)\tPROJECT: \(.[0]."grpc.request.glProjectPath")\tDURS: \(.[0]."grpc.time_ms"), \(.[1]."grpc.time_ms"), \(.[2]."grpc.time_ms")"' current jq --raw-output --slurp '
map(
select(
."grpc.request.glProjectPath" != null
and ."grpc.request.glProjectPath" != ""
and ."grpc.time_ms" != null
)
)
| group_by(."grpc.request.glProjectPath")
| sort_by(-length)
| limit(10; .[])
| sort_by(-."grpc.time_ms")
| [
length,
.[0]."grpc.time_ms",
.[1]."grpc.time_ms",
.[2]."grpc.time_ms",
.[0]."grpc.request.glProjectPath"
]
| @sh' /var/log/gitlab/gitaly/current \
| awk 'BEGIN { printf "%7s %10s %10s %10s\t%s\n", "CT", "MAX DURS", "", "", "PROJECT" }
{ printf "%7u %7u ms, %7u ms, %7u ms\t%s\n", $1, $2, $3, $4, $5 }'
``` ```
**Example output** **Example output**
```plaintext ```plaintext
CT: 635 PROJECT: groupA/project1 DURS: 4292.269, 4228.853, 2885.548 CT MAX DURS PROJECT
CT: 462 PROJECT: groupB/project5 DURS: 4368.981, 3623.553, 361.399 206 4898 ms, 1101 ms, 1032 ms 'groupD/project4'
CT: 455 PROJECT: groupC/project7 DURS: 387.295, 381.874, 373.988 109 1420 ms, 962 ms, 875 ms 'groupEF/project56'
663 106 ms, 96 ms, 94 ms 'groupABC/project123'
...
``` ```
...@@ -8,62 +8,81 @@ type: reference ...@@ -8,62 +8,81 @@ type: reference
# The .gitlab-ci.yml file # The .gitlab-ci.yml file
<!-- markdownlint-enable MD044 --> <!-- markdownlint-enable MD044 -->
To use GitLab CI/CD, you need an application codebase hosted in a To use GitLab CI/CD, you need:
Git repository, and for your build, test, and deployment
scripts to be specified in a file called [`.gitlab-ci.yml`](README.md), - Application code hosted in a Git repository.
located in the root path of your repository. - A file called [`.gitlab-ci.yml`](README.md) in the root of your repository, which
contains the CI/CD configuration.
In this file, you can define the scripts you want to run, define include and
cache dependencies, choose commands you want to run in sequence In the `.gitlab-ci.yml` file, you can define:
and those you want to run in parallel, define where you want to
deploy your app, and specify whether you want to run the scripts automatically - The scripts you want to run.
or trigger any of them manually. After you're familiar with - Other configuration files and templates you want to include.
GitLab CI/CD you can add more advanced steps into the configuration file. - Dependencies and caches.
- The commands you want to run in sequence and those you want to run in parallel.
To add scripts to that file, you need to organize them in a - The location to deploy your application to.
sequence that suits your application and are in accordance with - Whether you want to run the scripts automatically or trigger any of them manually.
the tests you wish to perform. To visualize the process, imagine
that all the scripts you add to the configuration file are the The scripts are grouped into **jobs**, and jobs run as part of a larger
same as the commands you run on a terminal on your computer. **pipeline**. You can group multiple independent jobs into **stages** that run in a defined order.
After you've added your `.gitlab-ci.yml` configuration file to your You should organize your jobs in a sequence that suits your application and is in accordance with
repository, GitLab detects it and run your scripts with the the tests you wish to perform. To [visualize](visualization.md) the process, imagine
tool called [GitLab Runner](https://docs.gitlab.com/runner/), which the scripts you add to jobs are the same as CLI commands you run on your computer.
works similarly to your terminal.
When you add a `.gitlab-ci.yml` file to your
The scripts are grouped into **jobs**, and together they compose repository, GitLab detects it and an application called [GitLab Runner](https://docs.gitlab.com/runner/)
a **pipeline**. A minimalist example of `.gitlab-ci.yml` file runs the scripts defined in the jobs.
could contain:
A `.gitlab-ci.yml` file might contain:
```yaml ```yaml
before_script: stages:
- apt-get install rubygems ruby-dev -y - build
- test
build-code-job:
stage: build
script:
- echo "Check the ruby version, then build some Ruby project files:"
- ruby -v
- rake
run-test: test-code-job1:
stage: test
script: script:
- ruby --version - echo "If the files are built successfully, test some files with one command:"
- rake test1
test-code-job2:
stage: test
script:
- echo "If the files are built successfully, test other files with a different command:"
- rake test2
``` ```
The `before_script` attribute would install the dependencies In this example, the `build-code-job` job in the `build` stage runs first. It outputs
for your app before running anything, and a **job** called the Ruby version the job is using, then runs `rake` to build project files.
`run-test` would print the Ruby version of the current system. If this job completes successfully, the two `test-code-job` jobs in the `test` stage start
Both of them compose a **pipeline** triggered at every push in parallel and run tests on the files.
to any branch of the repository.
The full pipeline in the example is composed of three jobs, grouped into two stages,
`build` and `test`. The pipeline runs every time changes are pushed to any
branch in the project.
GitLab CI/CD not only executes the jobs you've GitLab CI/CD not only executes the jobs but also shows you what's happening during execution,
set but also shows you what's happening during execution, as you just as you would see in your terminal:
would see in your terminal:
![job running](img/job_running.png) ![job running](img/job_running.png)
You create the strategy for your app and GitLab runs the pipeline You create the strategy for your app and GitLab runs the pipeline
for you according to what you've defined. Your pipeline status is also according to what you've defined. Your pipeline status is also
displayed by GitLab: displayed by GitLab:
![pipeline status](img/pipeline_status.png) ![pipeline status](img/pipeline_status.png)
At the end, if anything goes wrong, you can easily If anything goes wrong, you can
[roll back](../environments/index.md#retrying-and-rolling-back) all the changes: [roll back](../environments/index.md#retrying-and-rolling-back) the changes:
![rollback button](img/rollback.png) ![rollback button](img/rollback.png)
......
...@@ -129,7 +129,7 @@ of GitLab more efficient. ...@@ -129,7 +129,7 @@ of GitLab more efficient.
New information that would be useful toward the future usage or troubleshooting New information that would be useful toward the future usage or troubleshooting
of GitLab should not be written directly in a forum or other messaging system, of GitLab should not be written directly in a forum or other messaging system,
but added to a documentation MR and then referenced, as described above. Note but added to a documentation merge request and then referenced, as described above. Note
that among any other documentation changes, you can either: that among any other documentation changes, you can either:
- Add a [Troubleshooting section](#troubleshooting) to a doc if none exists. - Add a [Troubleshooting section](#troubleshooting) to a doc if none exists.
...@@ -141,7 +141,7 @@ the documentation helps others efficiently accomplish ...@@ -141,7 +141,7 @@ the documentation helps others efficiently accomplish
tasks and solve problems. tasks and solve problems.
If you have questions when considering, authoring, or editing documentation, ask If you have questions when considering, authoring, or editing documentation, ask
the Technical Writing team on Slack in `#docs` or in GitLab by mentioning the the Technical Writing team. They're available on Slack in `#docs` or in GitLab by mentioning the
writer for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/product-categories/#devops-stages). writer for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/product-categories/#devops-stages).
Otherwise, forge ahead with your best effort. It does not need to be perfect; Otherwise, forge ahead with your best effort. It does not need to be perfect;
the team is happy to review and improve upon your content. Review the the team is happy to review and improve upon your content. Review the
...@@ -160,7 +160,7 @@ Markdown rendering engine. For a complete Kramdown reference, see the ...@@ -160,7 +160,7 @@ Markdown rendering engine. For a complete Kramdown reference, see the
[GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/markdown-guide/). [GitLab Markdown Kramdown Guide](https://about.gitlab.com/handbook/markdown-guide/).
The [`gitlab-kramdown`](https://gitlab.com/gitlab-org/gitlab_kramdown) Ruby gem The [`gitlab-kramdown`](https://gitlab.com/gitlab-org/gitlab_kramdown) Ruby gem
will support all [GitLab Flavored Markdown](../../../user/markdown.md) in the future, which is plans to support all [GitLab Flavored Markdown](../../../user/markdown.md) in the future, which is
all Markdown supported for display in the GitLab application itself. For now, use all Markdown supported for display in the GitLab application itself. For now, use
regular Markdown, following the rules in the linked style guide. regular Markdown, following the rules in the linked style guide.
...@@ -256,7 +256,7 @@ Put files for a specific product area into the related folder: ...@@ -256,7 +256,7 @@ Put files for a specific product area into the related folder:
Refer to the following items when working with directories and files: Refer to the following items when working with directories and files:
1. When you create a new directory, always start with an `index.md` file. 1. When you create a new directory, always start with an `index.md` file.
Don't use another file name and _do not_ create `README.md` files. Don't use another filename and _do not_ create `README.md` files.
1. _Do not_ use special characters and spaces, or capital letters in file 1. _Do not_ use special characters and spaces, or capital letters in file
names, directory names, branch names, and anything that generates a path. names, directory names, branch names, and anything that generates a path.
1. When creating or renaming a file or directory and it has more than one word 1. When creating or renaming a file or directory and it has more than one word
...@@ -304,9 +304,9 @@ Do not include the same information in multiple places. ...@@ -304,9 +304,9 @@ Do not include the same information in multiple places.
### References across documents ### References across documents
- Give each folder an `index.md` page that introduces the topic, introduces the - Give each folder an `index.md` page that introduces the topic, and both introduces
pages within, and links to the pages within (including to the index pages of and links to the child pages, including to the index pages of
any next-level subpaths). any next-level sub-paths.
- To ensure discoverability, ensure each new or renamed doc is linked from its - To ensure discoverability, ensure each new or renamed doc is linked from its
higher-level index page and other related pages. higher-level index page and other related pages.
- When making reference to other GitLab products and features, link to their - When making reference to other GitLab products and features, link to their
...@@ -540,8 +540,8 @@ tenses, words, and phrases: ...@@ -540,8 +540,8 @@ tenses, words, and phrases:
- Don't overuse "that". In many cases, you can remove "that" from a sentence - Don't overuse "that". In many cases, you can remove "that" from a sentence
and improve readability. and improve readability.
- Avoid use of the future tense: - Avoid use of the future tense:
- Instead of "after you execute this command, GitLab will display the - Instead of `after you execute this command, GitLab will display the result`, use
result", use "after you execute this command, GitLab displays the result". `after you execute this command, GitLab displays the result`.
- Only use the future tense to convey when the action or result actually - Only use the future tense to convey when the action or result actually
occurs at a future time. occurs at a future time.
- Don't use slashes to clump different words together or as a replacement for - Don't use slashes to clump different words together or as a replacement for
...@@ -565,7 +565,7 @@ tenses, words, and phrases: ...@@ -565,7 +565,7 @@ tenses, words, and phrases:
<!-- vale gitlab.LatinTerms = YES --> <!-- vale gitlab.LatinTerms = YES -->
- Avoid using the word *currently* when talking about the product or its - Avoid using the word *currently* when talking about the product or its
features. The documentation describes the product as it is, and not as it features. The documentation describes the product as it is, and not as it
will be at some indeterminate point in the future. is planned to be in some indeterminate point in the future.
- Avoid using the word *scalability* when talking about increasing GitLab - Avoid using the word *scalability* when talking about increasing GitLab
performance for additional users. The words scale or scaling are sometimes performance for additional users. The words scale or scaling are sometimes
acceptable, but references to increasing GitLab performance for additional acceptable, but references to increasing GitLab performance for additional
...@@ -690,12 +690,12 @@ cp <your_source_directory> <your_destination_directory> ...@@ -690,12 +690,12 @@ cp <your_source_directory> <your_destination_directory>
Use the HTML `<kbd>` tag when referring to keystroke presses. For example: Use the HTML `<kbd>` tag when referring to keystroke presses. For example:
```plaintext ```plaintext
To stop the command, press <kbd>Ctrl</kbd>+<kbd>C</kbd>. To stop the command, press <kbd>Control</kbd>+<kbd>C</kbd>.
``` ```
When the docs are generated, the output is: When the docs are generated, the output is:
To stop the command, press <kbd>Ctrl</kbd>+<kbd>C</kbd>. To stop the command, press <kbd>Control</kbd>+<kbd>C</kbd>.
## Lists ## Lists
...@@ -732,7 +732,7 @@ This is a list of available features: ...@@ -732,7 +732,7 @@ This is a list of available features:
- Use dashes (`-`) for unordered lists instead of asterisks (`*`). - Use dashes (`-`) for unordered lists instead of asterisks (`*`).
- Prefix `1.` to every item in an ordered list. When rendered, the list items - Prefix `1.` to every item in an ordered list. When rendered, the list items
will appear with sequential numbering. display with sequential numbering.
### Punctuation ### Punctuation
...@@ -805,7 +805,7 @@ indentation: ...@@ -805,7 +805,7 @@ indentation:
- Unordered list item 3 - Unordered list item 3
```plaintext ```plaintext
a codeblock that will next inside list item 3 a code block that nests inside list item 3
``` ```
- Unordered list item 4 - Unordered list item 4
...@@ -828,7 +828,7 @@ For ordered lists, use three spaces for each level of indentation: ...@@ -828,7 +828,7 @@ For ordered lists, use three spaces for each level of indentation:
1. Ordered list item 3 1. Ordered list item 3
```plaintext ```plaintext
a codeblock that will next inside list item 3 a code block that nests inside list item 3
``` ```
1. Ordered list item 4 1. Ordered list item 4
...@@ -863,7 +863,7 @@ that's best described by a matrix, tables are the best choice. ...@@ -863,7 +863,7 @@ that's best described by a matrix, tables are the best choice.
### Creation guidelines ### Creation guidelines
Due to accessibility and scannability requirements, tables should not have any To keep tables accessible and scannable, tables should not have any
empty cells. If there is no otherwise meaningful value for a cell, consider entering empty cells. If there is no otherwise meaningful value for a cell, consider entering
*N/A* (for 'not applicable') or *none*. *N/A* (for 'not applicable') or *none*.
...@@ -909,7 +909,7 @@ For other punctuation rules, refer to the ...@@ -909,7 +909,7 @@ For other punctuation rules, refer to the
## Headings ## Headings
- Add _only one H1_ in each document, by adding `#` at the beginning of - Add _only one H1_ in each document, by adding `#` at the beginning of
it (when using Markdown). The `h1` will be the document `<title>`. it (when using Markdown). The `h1` becomes the document `<title>`.
- Start with an `h2` (`##`), and respect the order `h2` > `h3` > `h4` > `h5` > `h6`. - Start with an `h2` (`##`), and respect the order `h2` > `h3` > `h4` > `h5` > `h6`.
Never skip the hierarchy level, such as `h2` > `h4` Never skip the hierarchy level, such as `h2` > `h4`
- Avoid putting numbers in headings. Numbers shift, hence documentation anchor - Avoid putting numbers in headings. Numbers shift, hence documentation anchor
...@@ -974,7 +974,7 @@ sure to replace it with the new one. ...@@ -974,7 +974,7 @@ sure to replace it with the new one.
Important: Important:
- Avoid crosslinking documentation to headings unless you need to link to a - Avoid crosslinking documentation to headings unless you need to link to a
specific section of the document. This will avoid breaking anchors in the specific section of the document. This avoids breaking anchors in the
future in case the heading is changed. future in case the heading is changed.
- If possible, avoid changing headings since they're not only linked internally. - If possible, avoid changing headings since they're not only linked internally.
There are various links to GitLab documentation on the internet, such as There are various links to GitLab documentation on the internet, such as
...@@ -1023,7 +1023,7 @@ documentation in separate projects (for example, linking to Omnibus documentatio ...@@ -1023,7 +1023,7 @@ documentation in separate projects (for example, linking to Omnibus documentatio
from GitLab documentation), you must use absolute URLs. from GitLab documentation), you must use absolute URLs.
Do not use absolute URLs like `https://docs.gitlab.com/ee/index.html` to Do not use absolute URLs like `https://docs.gitlab.com/ee/index.html` to
crosslink to other documentation within the same project. Use relative links to cross-link to other documentation within the same project. Use relative links to
the file, like `../index.md`. (These are converted to HTML when the site is the file, like `../index.md`. (These are converted to HTML when the site is
rendered.) rendered.)
...@@ -1052,7 +1052,7 @@ To link to internal documentation: ...@@ -1052,7 +1052,7 @@ To link to internal documentation:
Do: `../../geo/replication/troubleshooting.md` Do: `../../geo/replication/troubleshooting.md`
- Always add the file name `file.md` at the end of the link with the `.md` - Always add the filename `file.md` at the end of the link with the `.md`
extension, not `.html`. extension, not `.html`.
Don't: Don't:
...@@ -1121,7 +1121,7 @@ Don't link directly to: ...@@ -1121,7 +1121,7 @@ Don't link directly to:
- Project features that require [special permissions](../../../user/permissions.md) - Project features that require [special permissions](../../../user/permissions.md)
to view. to view.
These will fail for: These fail for:
- Those without sufficient permissions. - Those without sufficient permissions.
- Automated link checkers. - Automated link checkers.
...@@ -1205,9 +1205,9 @@ When you take screenshots: ...@@ -1205,9 +1205,9 @@ When you take screenshots:
### Save the image ### Save the image
- Save the image with a lowercase file name that's descriptive of the feature - Save the image with a lowercase filename that's descriptive of the feature
or concept in the image. If the image is of the GitLab interface, append the or concept in the image. If the image is of the GitLab interface, append the
GitLab version to the file name, based on the following format: GitLab version to the filename, based on the following format:
`image_name_vX_Y.png`. For example, for a screenshot taken from the pipelines `image_name_vX_Y.png`. For example, for a screenshot taken from the pipelines
page of GitLab 11.1, a valid name is `pipelines_v11_1.png`. If you're adding an page of GitLab 11.1, a valid name is `pipelines_v11_1.png`. If you're adding an
illustration that doesn't include parts of the user interface, add the release illustration that doesn't include parts of the user interface, add the release
...@@ -1217,10 +1217,10 @@ When you take screenshots: ...@@ -1217,10 +1217,10 @@ When you take screenshots:
the `.md` document that you're working on is located. the `.md` document that you're working on is located.
- Consider using PNG images instead of JPEG. - Consider using PNG images instead of JPEG.
- [Compress all PNG images](#compress-images). - [Compress all PNG images](#compress-images).
- Compress gifs with <https://ezgif.com/optimize> or similar tool. - Compress GIFs with <https://ezgif.com/optimize> or similar tool.
- Images should be used (only when necessary) to _illustrate_ the description - Images should be used (only when necessary) to _illustrate_ the description
of a process, not to _replace_ it. of a process, not to _replace_ it.
- Max image size: 100KB (gifs included). - Max image size: 100KB (GIFs included).
- See also how to link and embed [videos](#videos) to illustrate the - See also how to link and embed [videos](#videos) to illustrate the
documentation. documentation.
...@@ -1360,7 +1360,7 @@ hidden on the documentation site, but is displayed by `/help`. ...@@ -1360,7 +1360,7 @@ hidden on the documentation site, but is displayed by `/help`.
and leave a blank line between the command and the output. and leave a blank line between the command and the output.
- When providing a command without output, don't prefix the shell command with `$`. - When providing a command without output, don't prefix the shell command with `$`.
- If you need to include triple backticks inside a code block, use four backticks - If you need to include triple backticks inside a code block, use four backticks
for the codeblock fences instead of three. for the code block fences instead of three.
- For regular fenced code blocks, always use a highlighting class corresponding to - For regular fenced code blocks, always use a highlighting class corresponding to
the language for better readability. Examples: the language for better readability. Examples:
...@@ -1397,7 +1397,7 @@ of available language classes: ...@@ -1397,7 +1397,7 @@ of available language classes:
| `graphql` | | | `graphql` | |
| `haml` | | | `haml` | |
| `html` | | | `html` | |
| `ini` | For some simple config files that are not in TOML format. | | `ini` | For some simple configuration files that are not in TOML format. |
| `javascript` | Alias `js`. | | `javascript` | Alias `js`. |
| `json` | | | `json` | |
| `markdown` | Alias: `md`. | | `markdown` | Alias: `md`. |
...@@ -1405,7 +1405,7 @@ of available language classes: ...@@ -1405,7 +1405,7 @@ of available language classes:
| `nginx` | | | `nginx` | |
| `perl` | | | `perl` | |
| `php` | | | `php` | |
| `plaintext` | Examples with no defined language, such as output from shell commands or API calls. If a codeblock has no language, it defaults to `plaintext`. Alias: `text`. | | `plaintext` | Examples with no defined language, such as output from shell commands or API calls. If a code block has no language, it defaults to `plaintext`. Alias: `text`.|
| `prometheus` | Prometheus configuration examples. | | `prometheus` | Prometheus configuration examples. |
| `python` | | | `python` | |
| `ruby` | Alias: `rb`. | | `ruby` | Alias: `rb`. |
...@@ -1435,7 +1435,7 @@ Usage examples: ...@@ -1435,7 +1435,7 @@ Usage examples:
Example: `**{tanuki}**` renders as: **{tanuki}**. Example: `**{tanuki}**` renders as: **{tanuki}**.
- Icon with custom size: `**{icon-name, size}**` - Icon with custom size: `**{icon-name, size}**`
Available sizes (in px): 8, 10, 12, 14, 16, 18, 24, 32, 48, and 72 Available sizes (in pixels): 8, 10, 12, 14, 16, 18, 24, 32, 48, and 72
Example: `**{tanuki, 24}**` renders as: **{tanuki, 24}**. Example: `**{tanuki, 24}**` renders as: **{tanuki, 24}**.
- Icon with custom size and class: `**{icon-name, size, class-name}**`. - Icon with custom size and class: `**{icon-name, size, class-name}**`.
...@@ -1488,7 +1488,7 @@ When you need to call special attention to particular sentences, use the ...@@ -1488,7 +1488,7 @@ When you need to call special attention to particular sentences, use the
following markup to create highlighted alert boxes. following markup to create highlighted alert boxes.
Alert boxes work for one paragraph only. Multiple paragraphs, lists, and headers Alert boxes work for one paragraph only. Multiple paragraphs, lists, and headers
won't render correctly. For multiple lines, use [blockquotes](#blockquotes) don't render correctly. For multiple lines, use [blockquotes](#blockquotes)
instead. instead.
Alert boxes render only on the GitLab documentation site (<https://docs.gitlab.com>). Alert boxes render only on the GitLab documentation site (<https://docs.gitlab.com>).
...@@ -1609,7 +1609,7 @@ documentation authors on agreed styles and usage of terms. ...@@ -1609,7 +1609,7 @@ documentation authors on agreed styles and usage of terms.
### Merge requests (MRs) ### Merge requests (MRs)
Merge requests allow you to exchange changes you made to source code and Merge requests allow you to exchange changes you made to source code and
collaborate with other people on the same project. You'll see this term used in collaborate with other people on the same project. This term is used in
the following ways: the following ways:
- Use lowercase _merge requests_ regardless of whether referring to the feature - Use lowercase _merge requests_ regardless of whether referring to the feature
...@@ -1782,7 +1782,7 @@ For example: ...@@ -1782,7 +1782,7 @@ For example:
### Removing versions after each major release ### Removing versions after each major release
Whenever a major GitLab release occurs, we will remove all version references Whenever a major GitLab release occurs, we remove all version references
to now-unsupported versions of GitLab. Note that this includes the removal of to now-unsupported versions of GitLab. Note that this includes the removal of
specific instructions for users of non-supported GitLab versions. For example, specific instructions for users of non-supported GitLab versions. For example,
if we're currently supporting GitLab versions 11.x through 13.x, special if we're currently supporting GitLab versions 11.x through 13.x, special
...@@ -1854,12 +1854,12 @@ self-managed instances. If so, include the relevant "`TIER` ONLY" ...@@ -1854,12 +1854,12 @@ self-managed instances. If so, include the relevant "`TIER` ONLY"
#### Product badge display behavior #### Product badge display behavior
When using the tier markup with headers, the documentation page will display the When using the tier markup with headers, the documentation page displays the
full tier badge with the header line. full tier badge with the header line.
You can also use the tier markup with paragraphs, list items, and table cells. You can also use the tier markup with paragraphs, list items, and table cells.
For these cases, the tier mention will be represented by an orange info icon For these cases, the tier mention is represented by an information icon
**{information}** that will display the tiers when visitors point to the icon. that displays the tiers when visitors point to the icon.
For example: For example:
- `**(STARTER)**` displays as **(STARTER)** - `**(STARTER)**` displays as **(STARTER)**
...@@ -1869,9 +1869,9 @@ For example: ...@@ -1869,9 +1869,9 @@ For example:
#### How it works #### How it works
Introduced by [!244](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/244), Introduced by [!244](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/244),
the special markup `**(STARTER)**` will generate a `span` element to trigger the the special markup `**(STARTER)**` generates a `span` element to trigger the
badges and tooltips (`<span class="badge-trigger starter">`). When the keyword badges and tooltips (`<span class="badge-trigger starter">`). When the keyword
_only_ is added, the corresponding GitLab.com badge will not be displayed. _only_ is added, the corresponding GitLab.com badge isn't displayed.
## Specific sections ## Specific sections
...@@ -1883,7 +1883,7 @@ sections are outlined in this section. ...@@ -1883,7 +1883,7 @@ sections are outlined in this section.
There are many cases that a restart/reconfigure of GitLab is required. To avoid There are many cases that a restart/reconfigure of GitLab is required. To avoid
duplication, link to the special document that can be found in duplication, link to the special document that can be found in
[`doc/administration/restart_gitlab.md`](../../../administration/restart_gitlab.md). [`doc/administration/restart_gitlab.md`](../../../administration/restart_gitlab.md).
Usually the text will read like: Usually the text reads like:
```markdown ```markdown
Save the file and [reconfigure GitLab](../../../administration/restart_gitlab.md) Save the file and [reconfigure GitLab](../../../administration/restart_gitlab.md)
...@@ -1899,8 +1899,8 @@ If the document you are editing resides in a place other than the GitLab CE/EE ...@@ -1899,8 +1899,8 @@ If the document you are editing resides in a place other than the GitLab CE/EE
**Ruby:** **Ruby:**
In [step 2 of the installation guide](../../../install/installation.md#2-ruby), In [step 2 of the installation guide](../../../install/installation.md#2-ruby),
we install Ruby from source. Whenever there is a new version that needs to we install Ruby from source. When a version update is needed,
be updated, remember to change it throughout the codeblock and also replace remember to change it throughout the code block and also replace
the sha256sum (it can be found in the [downloads page](https://www.ruby-lang.org/en/downloads/) the sha256sum (it can be found in the [downloads page](https://www.ruby-lang.org/en/downloads/)
of the Ruby website). of the Ruby website).
...@@ -1954,7 +1954,7 @@ In this case: ...@@ -1954,7 +1954,7 @@ In this case:
methods. methods.
- The code blocks are indented one or more spaces under the list item to render - The code blocks are indented one or more spaces under the list item to render
correctly. correctly.
- Different highlighting languages are used for each config in the code block. - Different highlighting languages are used for each configuration in the code block.
- The [GitLab Restart](#gitlab-restart) section is used to explain a required - The [GitLab Restart](#gitlab-restart) section is used to explain a required
restart or reconfigure of GitLab. restart or reconfigure of GitLab.
......
...@@ -22,7 +22,7 @@ Our goal is to replace one by one all inline SVG Icons (as those currently bloat ...@@ -22,7 +22,7 @@ Our goal is to replace one by one all inline SVG Icons (as those currently bloat
### Usage in HAML/Rails ### Usage in HAML/Rails
To use a sprite Icon in HAML or Rails we use a specific helper function : To use a sprite Icon in HAML or Rails we use a specific helper function:
```ruby ```ruby
sprite_icon(icon_name, size: nil, css_class: '') sprite_icon(icon_name, size: nil, css_class: '')
...@@ -48,6 +48,8 @@ sprite_icon(icon_name, size: nil, css_class: '') ...@@ -48,6 +48,8 @@ sprite_icon(icon_name, size: nil, css_class: '')
</svg> </svg>
``` ```
**Please note:** The `icon(icon_name, options: {})` helper function is deprecated and should not be used anymore.
### Usage in Vue ### Usage in Vue
[GitLab UI](https://gitlab-org.gitlab.io/gitlab-ui/), our components library, provides a component to display sprite icons. [GitLab UI](https://gitlab-org.gitlab.io/gitlab-ui/), our components library, provides a component to display sprite icons.
......
...@@ -115,7 +115,7 @@ Add the following to your `.gitlab-ci.yml` file: ...@@ -115,7 +115,7 @@ Add the following to your `.gitlab-ci.yml` file:
```yaml ```yaml
include: include:
- template: Secret-Detection.gitlab-ci.yml - template: Security/Secret-Detection.gitlab-ci.yml
``` ```
The included template creates Secret Detection jobs in your CI/CD pipeline and scans The included template creates Secret Detection jobs in your CI/CD pipeline and scans
...@@ -153,7 +153,7 @@ override the `secret_detection` job with the `SECRET_DETECTION_HISTORIC_SCAN` va ...@@ -153,7 +153,7 @@ override the `secret_detection` job with the `SECRET_DETECTION_HISTORIC_SCAN` va
```yaml ```yaml
include: include:
- template: Secret-Detection.gitlab-ci.yml - template: Security/Secret-Detection.gitlab-ci.yml
secret_detection: secret_detection:
variables: variables:
......
<script> <script>
import { GlTooltipDirective, GlLink, GlButton, GlIcon } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue'; import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
export default { export default {
components: { components: {
GlDropdown,
GlDropdownItem,
GlIcon, GlIcon,
ProjectAvatar, ProjectAvatar,
GlLink, GlLink,
GlButton,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -32,9 +33,9 @@ export default { ...@@ -32,9 +33,9 @@ export default {
<template> <template>
<div <div
class="d-flex align-items-center page-title-holder text-secondary justify-content-between pb-2 mb-3" class="gl-display-flex gl-align-items-center page-title-holder text-secondary gl-justify-content-space-between pb-2 mb-3"
> >
<div class="d-flex align-items-center"> <div class="gl-display-flex gl-align-items-center">
<project-avatar :project="project.namespace" :size="20" class="flex-shrink-0" /> <project-avatar :project="project.namespace" :size="20" class="flex-shrink-0" />
<gl-link class="js-namespace-link text-secondary" :href="`/${project.namespace.full_path}`"> <gl-link class="js-namespace-link text-secondary" :href="`/${project.namespace.full_path}`">
<span class="js-namespace gl-mr-3"> {{ project.namespace.name }} </span> <span class="js-namespace gl-mr-3"> {{ project.namespace.name }} </span>
...@@ -45,23 +46,23 @@ export default { ...@@ -45,23 +46,23 @@ export default {
<span class="js-name gl-mr-3"> {{ project.name }} </span> <span class="js-name gl-mr-3"> {{ project.name }} </span>
</gl-link> </gl-link>
</div> </div>
<div class="dropdown js-more-actions"> <div class="gl-display-flex js-more-actions">
<button <gl-dropdown
v-gl-tooltip toggle-class="js-more-actions-toggle gl-display-flex gl-align-items-center gl-px-3! gl-bg-transparent gl-shadow-none!"
class="js-more-actions-toggle d-flex align-items-center ml-2 btn btn-transparent" right
type="button"
data-toggle="dropdown"
:title="$options.moreActionsText"
> >
<gl-icon name="ellipsis_v" class="text-secondary" /> <template #button-content>
</button> <gl-icon
<ul class="dropdown-menu dropdown-menu-right"> v-gl-tooltip
<li> :title="$options.moreActionsText"
<gl-button class="js-remove-button" variant="link" @click="onRemove()"> name="ellipsis_v"
<span class="text-danger"> {{ $options.removeProjectText }} </span> class="text-secondary"
</gl-button> />
</li> </template>
</ul> <gl-dropdown-item class="js-remove-button" variant="link" @click="onRemove()">
<span class="text-danger"> {{ $options.removeProjectText }} </span>
</gl-dropdown-item>
</gl-dropdown>
</div> </div>
</div> </div>
</template> </template>
...@@ -23,8 +23,7 @@ module BillingPlansHelper ...@@ -23,8 +23,7 @@ module BillingPlansHelper
end end
def use_new_purchase_flow?(namespace) def use_new_purchase_flow?(namespace)
namespace.group? && namespace.group? && (namespace.actual_plan_name == Plan::FREE || namespace.trial_active?)
namespace.actual_plan_name == Plan::FREE
end end
def show_contact_sales_button?(purchase_link_action) def show_contact_sales_button?(purchase_link_action)
......
...@@ -39,6 +39,22 @@ module EE ...@@ -39,6 +39,22 @@ module EE
scope :include_gitlab_subscription_with_hosted_plan, -> { includes(gitlab_subscription: :hosted_plan) } scope :include_gitlab_subscription_with_hosted_plan, -> { includes(gitlab_subscription: :hosted_plan) }
scope :join_gitlab_subscription, -> { joins("LEFT OUTER JOIN gitlab_subscriptions ON gitlab_subscriptions.namespace_id=namespaces.id") } scope :join_gitlab_subscription, -> { joins("LEFT OUTER JOIN gitlab_subscriptions ON gitlab_subscriptions.namespace_id=namespaces.id") }
scope :top_most, -> { where(parent_id: nil) }
scope :in_active_trial, -> do
left_joins(gitlab_subscription: :hosted_plan)
.where(gitlab_subscriptions: { trial: true, trial_ends_on: Date.today.. })
end
scope :in_default_plan, -> do
left_joins(gitlab_subscription: :hosted_plan)
.where(plans: { name: [nil, *::Plan.default_plans] })
end
scope :eligible_for_subscription, -> do
top_most.in_active_trial.or(top_most.in_default_plan)
end
scope :eligible_for_trial, -> do scope :eligible_for_trial, -> do
left_joins(gitlab_subscription: :hosted_plan) left_joins(gitlab_subscription: :hosted_plan)
.where( .where(
......
...@@ -256,11 +256,7 @@ module EE ...@@ -256,11 +256,7 @@ module EE
end end
def manageable_groups_eligible_for_subscription def manageable_groups_eligible_for_subscription
manageable_groups manageable_groups.eligible_for_subscription.order(:name)
.where(parent_id: nil)
.left_joins(:gitlab_subscription)
.merge(GitlabSubscription.left_joins(:hosted_plan).where(plans: { name: [nil, *::Plan.default_plans] }))
.order(:name)
end end
def manageable_groups_eligible_for_trial def manageable_groups_eligible_for_trial
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
.form-group.gl-mb-3 .form-group.gl-mb-3
.form-check .form-check
= f.check_box :prevent_forking_outside_group, checked: group.prevent_forking_outside_group?, class: 'form-check-input', disabled: !can_change_prevent_forking?(current_user, group) = f.check_box :prevent_forking_outside_group, checked: group.prevent_forking_outside_group?, class: 'form-check-input', disabled: !can_change_prevent_forking?(current_user, group), data: { qa_selector: 'prevent_forking_outside_group_checkbox' }
= f.label :prevent_forking_outside_group, class: 'form-check-label' do = f.label :prevent_forking_outside_group, class: 'form-check-label' do
%span.gl-display-block= s_('GroupSettings|Prevent forking outside of the group') %span.gl-display-block= s_('GroupSettings|Prevent forking outside of the group')
%span.text-muted= s_('GroupSettings|This setting will prevent group members from forking projects outside of the group.') %span.text-muted= s_('GroupSettings|This setting will prevent group members from forking projects outside of the group.')
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
- if project.archived? - if project.archived?
.text-warning.center.prepend-top-20 .text-warning.center.prepend-top-20
%p %p
= icon("exclamation-triangle fw") = sprite_icon('warning-solid')
= _('Archived project! Repository and other project resources are read-only') = _('Archived project! Repository and other project resources are read-only')
- if project.marked_for_deletion? - if project.marked_for_deletion?
.text-warning.center.prepend-top-20 .text-warning.center.prepend-top-20
%p %p
= icon("exclamation-triangle fw") = sprite_icon('warning-solid')
= _("Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only.") % { date: permanent_deletion_date(project.marked_for_deletion_at) } = _("Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only.") % { date: permanent_deletion_date(project.marked_for_deletion_at) }
- if @project.mirror_ever_updated_successfully? && @repository.diverged_from_upstream?(branch.name) - if @project.mirror_ever_updated_successfully? && @repository.diverged_from_upstream?(branch.name)
%span.badge.badge-danger.gl-ml-2.has-tooltip{ data: { html: "true", title: branch_diverged_tooltip_message } } %span.badge.badge-danger.gl-ml-2.has-tooltip{ data: { html: "true", title: branch_diverged_tooltip_message } }
= icon('exclamation-triangle') = sprite_icon('warning-solid')
= s_('Branches|diverged from upstream') = s_('Branches|diverged from upstream')
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if @ref.present? && @project.mirror_ever_updated_successfully? && @repository.diverged_from_upstream?(@ref) - if @ref.present? && @project.mirror_ever_updated_successfully? && @repository.diverged_from_upstream?(@ref)
%span.has-tooltip{ data: { html: "true", title: branch_diverged_tooltip_message } } %span.has-tooltip{ data: { html: "true", title: branch_diverged_tooltip_message } }
= icon('exclamation-triangle') = sprite_icon('warning-solid')
This branch has diverged from upstream. This branch has diverged from upstream.
.project-mirror-button .project-mirror-button
= render "shared/mirror_update_button" = render "shared/mirror_update_button"
- error_messages = @project.repository_size_checker.error_message - error_messages = @project.repository_size_checker.error_message
%h4.size-limit-reached %h4.size-limit-reached
= icon("exclamation-triangle") = sprite_icon('warning-solid')
= error_messages.merge_error = error_messages.merge_error
%p %p
......
.card.card-without-border %ul.content-list.issuable-list
%ul.content-list.issuable-list = render partial: 'groups/epics/epic', collection: @epics
= render partial: 'groups/epics/epic', collection: @epics
= paginate_collection @epics = paginate_collection @epics
---
title: Remove Bootstrap 4's Cards class name from Epics
merge_request: 48856
author: Takuya Noguchi
type: performance
---
title: New subscription purchase for trial namespaces follow new flow
merge_request: 47880
author:
type: changed
...@@ -200,14 +200,6 @@ module EE ...@@ -200,14 +200,6 @@ module EE
ldap_keys: count(::LDAPKey), ldap_keys: count(::LDAPKey),
ldap_users: count(::User.ldap, 'users.id'), ldap_users: count(::User.ldap, 'users.id'),
pod_logs_usages_total: redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] }, pod_logs_usages_total: redis_usage_data { ::Gitlab::UsageCounters::PodLogs.usage_totals[:total] },
projects_enforcing_code_owner_approval: count(::Project.without_deleted.non_archived.requiring_code_owner_approval),
merge_requests_with_added_rules: distinct_count(::ApprovalMergeRequestRule.with_added_approval_rules,
:merge_request_id,
start: approval_merge_request_rule_minimum_id,
finish: approval_merge_request_rule_maximum_id),
merge_requests_with_optional_codeowners: distinct_count(::ApprovalMergeRequestRule.code_owner_approval_optional, :merge_request_id),
merge_requests_with_overridden_project_rules: merge_requests_with_overridden_project_rules,
merge_requests_with_required_codeowners: distinct_count(::ApprovalMergeRequestRule.code_owner_approval_required, :merge_request_id),
merged_merge_requests_using_approval_rules: count(::MergeRequest.merged.joins(:approval_rules), # rubocop: disable CodeReuse/ActiveRecord merged_merge_requests_using_approval_rules: count(::MergeRequest.merged.joins(:approval_rules), # rubocop: disable CodeReuse/ActiveRecord
start: merge_request_minimum_id, start: merge_request_minimum_id,
finish: merge_request_maximum_id), finish: merge_request_maximum_id),
......
import { GlTooltip, GlIcon } from '@gitlab/ui'; import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Vue from 'vue'; import Vue from 'vue';
import DeployBoard from 'ee/environments/components/deploy_board_component.vue'; import DeployBoard from 'ee/environments/components/deploy_board_component.vue';
...@@ -95,7 +95,7 @@ describe('Deploy Board', () => { ...@@ -95,7 +95,7 @@ describe('Deploy Board', () => {
}); });
it('should render loading spinner', () => { it('should render loading spinner', () => {
expect(wrapper.find('.fa-spin')).toBeDefined(); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
}); });
}); });
......
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
exports[`Project Header matches the snapshot 1`] = ` exports[`Project Header matches the snapshot 1`] = `
<div <div
class="d-flex align-items-center page-title-holder text-secondary justify-content-between pb-2 mb-3" class="gl-display-flex gl-align-items-center page-title-holder text-secondary gl-justify-content-space-between pb-2 mb-3"
> >
<div <div
class="d-flex align-items-center" class="gl-display-flex gl-align-items-center"
> >
<project-avatar-stub <project-avatar-stub
class="flex-shrink-0" class="flex-shrink-0"
...@@ -48,41 +48,35 @@ exports[`Project Header matches the snapshot 1`] = ` ...@@ -48,41 +48,35 @@ exports[`Project Header matches the snapshot 1`] = `
</div> </div>
<div <div
class="dropdown js-more-actions" class="gl-display-flex js-more-actions"
> >
<button <gl-dropdown-stub
class="js-more-actions-toggle d-flex align-items-center ml-2 btn btn-transparent" category="primary"
data-toggle="dropdown" headertext=""
title="More actions" right=""
type="button" size="medium"
text=""
toggleclass="js-more-actions-toggle gl-display-flex gl-align-items-center gl-px-3! gl-bg-transparent gl-shadow-none!"
variant="default"
> >
<gl-icon-stub
class="text-secondary" <gl-dropdown-item-stub
name="ellipsis_v" avatarurl=""
size="16" class="js-remove-button"
/> iconcolor=""
</button> iconname=""
iconrightarialabel=""
<ul iconrightname=""
class="dropdown-menu dropdown-menu-right" secondarytext=""
> variant="link"
<li> >
<gl-button-stub <span
buttontextclasses="" class="text-danger"
category="primary"
class="js-remove-button"
icon=""
size="medium"
variant="link"
> >
<span Remove
class="text-danger" </span>
> </gl-dropdown-item-stub>
Remove </gl-dropdown-stub>
</span>
</gl-button-stub>
</li>
</ul>
</div> </div>
</div> </div>
`; `;
import { GlButton } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import component from 'ee/environments_dashboard/components/dashboard/project_header.vue'; import component from 'ee/environments_dashboard/components/dashboard/project_header.vue';
import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue'; import ProjectAvatar from '~/vue_shared/components/project_avatar/default.vue';
...@@ -66,21 +66,18 @@ describe('Project Header', () => { ...@@ -66,21 +66,18 @@ describe('Project Header', () => {
describe('more actions', () => { describe('more actions', () => {
it('should list "remove" as an action', () => { it('should list "remove" as an action', () => {
const removeLink = wrapper const removeLink = wrapper
.find('.dropdown-menu') .find(GlDropdown)
.findAll('li') .findAll(GlDropdownItem)
.filter(w => w.text() === 'Remove'); .filter(w => w.text() === 'Remove');
expect(removeLink.exists()).toBe(true); expect(removeLink.exists()).toBe(true);
}); });
it('should emit a "remove" event when "remove" is clicked', () => { it('should emit a "remove" event when "remove" is clicked', () => {
const removeLink = wrapper const removeLink = wrapper
.find('.dropdown-menu') .find(GlDropdown)
.findAll('li') .findAll(GlDropdownItem)
.filter(w => w.text() === 'Remove'); .filter(w => w.text() === 'Remove');
removeLink removeLink.at(0).vm.$emit('click');
.at(0)
.find(GlButton)
.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('remove')).toContainEqual([propsData.project.remove_path]); expect(wrapper.emitted('remove')).toContainEqual([propsData.project.remove_path]);
......
...@@ -57,7 +57,8 @@ RSpec.describe BillingPlansHelper do ...@@ -57,7 +57,8 @@ RSpec.describe BillingPlansHelper do
describe '#use_new_purchase_flow?' do describe '#use_new_purchase_flow?' do
where type: ['Group', nil], where type: ['Group', nil],
plan: Plan.all_plans plan: Plan.all_plans,
trial_active: [true, false]
with_them do with_them do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
...@@ -68,12 +69,13 @@ RSpec.describe BillingPlansHelper do ...@@ -68,12 +69,13 @@ RSpec.describe BillingPlansHelper do
before do before do
allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:current_user).and_return(user)
allow(namespace).to receive(:trial_active?).and_return(trial_active)
end end
subject { helper.use_new_purchase_flow?(namespace) } subject { helper.use_new_purchase_flow?(namespace) }
it do it do
result = type == 'Group' && plan == Plan::FREE result = type == 'Group' && (plan == Plan::FREE || trial_active)
is_expected.to be(result) is_expected.to be(result)
end end
......
...@@ -219,24 +219,6 @@ RSpec.describe Gitlab::UsageData do ...@@ -219,24 +219,6 @@ RSpec.describe Gitlab::UsageData do
end end
end end
describe 'code owner approval required' do
before do
create(:protected_branch, code_owner_approval_required: true)
create(:protected_branch,
code_owner_approval_required: true,
project: create(:project, :archived))
create(:protected_branch,
code_owner_approval_required: true,
project: create(:project, pending_delete: true))
end
it 'counts the projects actively requiring code owner approval' do
expect(described_class.system_usage_data[:counts][:projects_enforcing_code_owner_approval]).to eq(1)
end
end
describe 'merge requests merged using approval rules' do describe 'merge requests merged using approval rules' do
before do before do
create(:approval_merge_request_rule, merge_request: create(:merge_request, :merged)) create(:approval_merge_request_rule, merge_request: create(:merge_request, :merged))
......
...@@ -171,6 +171,130 @@ RSpec.describe Namespace do ...@@ -171,6 +171,130 @@ RSpec.describe Namespace do
end end
end end
describe '.top_most' do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:sub_namespace) { create(:namespace, parent: namespace) }
subject { described_class.top_most.ids }
it 'only contains root namespace' do
is_expected.to eq([namespace.id])
end
end
describe '.in_active_trial' do
let_it_be(:namespaces) do
[
create(:namespace),
create(:namespace_with_plan),
create(:namespace_with_plan, trial_ends_on: Date.tomorrow)
]
end
it 'is consistent to trial_active? method' do
namespaces.each do |ns|
consistent = described_class.in_active_trial.include?(ns) == !!ns.trial_active?
expect(consistent).to be true
end
end
end
describe '.in_default_plan' do
subject { described_class.in_default_plan.ids }
where(:plan_name, :expect_in_default_plan) do
::Plan::FREE | true
::Plan::DEFAULT | true
::Plan::BRONZE | false
::Plan::SILVER | false
::Plan::GOLD | false
end
with_them do
it 'returns expected result' do
namespace = create(:namespace_with_plan, plan: "#{plan_name}_plan")
is_expected.to eq(expect_in_default_plan ? [namespace.id] : [])
end
end
it 'includes namespace with no subscription' do
namespace = create(:namespace)
is_expected.to eq([namespace.id])
end
end
describe '.eligible_for_subscription' do
let_it_be(:namespace) { create :namespace }
let_it_be(:sub_namespace) { create(:namespace, parent: namespace) }
subject { described_class.eligible_for_subscription.ids }
context 'when there is no subscription' do
it { is_expected.to eq([namespace.id]) }
end
context 'when there is a subscription' do
context 'with a plan that is eligible for a trial' do
where(plan: ::Plan::PLANS_ELIGIBLE_FOR_TRIAL)
with_them do
context 'and has not yet been trialed' do
before do
create :gitlab_subscription, plan, namespace: namespace
create :gitlab_subscription, plan, namespace: sub_namespace
end
it { is_expected.to eq([namespace.id]) }
end
context 'but has already had a trial' do
before do
create :gitlab_subscription, plan, :expired_trial, namespace: namespace
create :gitlab_subscription, plan, :expired_trial, namespace: sub_namespace
end
it { is_expected.to eq([namespace.id]) }
end
context 'but is currently being trialed' do
before do
create :gitlab_subscription, plan, :active_trial, namespace: namespace
create :gitlab_subscription, plan, :active_trial, namespace: sub_namespace
end
it { is_expected.to eq([namespace.id]) }
end
end
end
context 'in active trial gold plan' do
before do
create :gitlab_subscription, ::Plan::GOLD, :active_trial, namespace: namespace
create :gitlab_subscription, ::Plan::GOLD, :active_trial, namespace: sub_namespace
end
it { is_expected.to eq([namespace.id]) }
end
context 'with a paid plan and not in trial' do
where(plan: ::Plan::PAID_HOSTED_PLANS)
with_them do
context 'and has not yet been trialed' do
before do
create :gitlab_subscription, plan, namespace: namespace
end
it { is_expected.to be_empty }
end
end
end
end
end
describe '.eligible_for_trial' do describe '.eligible_for_trial' do
let_it_be(:namespace) { create :namespace } let_it_be(:namespace) { create :namespace }
......
...@@ -1019,6 +1019,7 @@ RSpec.describe User do ...@@ -1019,6 +1019,7 @@ RSpec.describe User do
let_it_be(:free_group_z) { create(:group, name: 'AZ', gitlab_subscription: create(:gitlab_subscription, :free)) } let_it_be(:free_group_z) { create(:group, name: 'AZ', gitlab_subscription: create(:gitlab_subscription, :free)) }
let_it_be(:free_group_a) { create(:group, name: 'AA', gitlab_subscription: create(:gitlab_subscription, :free)) } let_it_be(:free_group_a) { create(:group, name: 'AA', gitlab_subscription: create(:gitlab_subscription, :free)) }
let_it_be(:sub_group) { create(:group, name: 'SubGroup', parent: free_group_a) } let_it_be(:sub_group) { create(:group, name: 'SubGroup', parent: free_group_a) }
let_it_be(:trial_group) { create(:group, name: 'AB', gitlab_subscription: create(:gitlab_subscription, :active_trial, :gold)) }
subject { user.manageable_groups_eligible_for_subscription } subject { user.manageable_groups_eligible_for_subscription }
...@@ -1068,6 +1069,30 @@ RSpec.describe User do ...@@ -1068,6 +1069,30 @@ RSpec.describe User do
it { is_expected.not_to include(sub_group) } it { is_expected.not_to include(sub_group) }
end end
context 'developer of a trial group' do
before do
trial_group.add_developer(user)
end
it { is_expected.not_to include(trial_group) }
end
context 'owner of a trial group' do
before do
trial_group.add_owner(user)
end
it { is_expected.to include(trial_group) }
end
context 'maintainer of a trial group' do
before do
trial_group.add_maintainer(user)
end
it { is_expected.to include(trial_group) }
end
end end
describe '#manageable_groups_eligible_for_trial' do describe '#manageable_groups_eligible_for_trial' do
......
...@@ -77,6 +77,9 @@ module Gitlab ...@@ -77,6 +77,9 @@ module Gitlab
default_to_issues_board: { default_to_issues_board: {
tracking_category: 'Growth::Conversion::Experiment::DefaultToIssuesBoard', tracking_category: 'Growth::Conversion::Experiment::DefaultToIssuesBoard',
use_backwards_compatible_subject_index: true use_backwards_compatible_subject_index: true
},
jobs_empty_state: {
tracking_category: 'Growth::Activation::Experiment::JobsEmptyState'
} }
}.freeze }.freeze
......
...@@ -26,11 +26,11 @@ module Gitlab ...@@ -26,11 +26,11 @@ module Gitlab
private private
def keyset_pagination_enabled? def keyset_pagination_enabled?
Feature.enabled?(:branch_list_keyset_pagination, project) && params[:pagination] == 'keyset' Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) && params[:pagination] == 'keyset'
end end
def paginate_first_page? def paginate_first_page?
Feature.enabled?(:branch_list_keyset_pagination, project) && (params[:page].blank? || params[:page].to_i == 1) Feature.enabled?(:branch_list_keyset_pagination, project, default_enabled: true) && (params[:page].blank? || params[:page].to_i == 1)
end end
def paginate_via_gitaly(finder) def paginate_via_gitaly(finder)
......
...@@ -15527,6 +15527,18 @@ msgstr "" ...@@ -15527,6 +15527,18 @@ msgstr ""
msgid "Jobs|Are you sure you want to retry this job?" msgid "Jobs|Are you sure you want to retry this job?"
msgstr "" msgstr ""
msgid "Jobs|Create CI/CD configuration file"
msgstr ""
msgid "Jobs|Jobs are the building blocks of a GitLab CI/CD pipeline. Each job has a specific task, like testing code. To set up jobs in a CI/CD pipeline, add a CI/CD configuration file to your project."
msgstr ""
msgid "Jobs|No jobs to show"
msgstr ""
msgid "Jobs|Use jobs to automate your tasks"
msgstr ""
msgid "Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment. Retrying this job could result in overwriting the environment with the older source code." msgid "Jobs|You're about to retry a job that failed because it attempted to deploy code that is older than the latest deployment. Retrying this job could result in overwriting the environment with the older source code."
msgstr "" msgstr ""
......
...@@ -29,6 +29,10 @@ module QA ...@@ -29,6 +29,10 @@ module QA
element :membership_lock_checkbox element :membership_lock_checkbox
end end
view 'ee/app/views/groups/settings/_prevent_forking.html.haml' do
element :prevent_forking_outside_group_checkbox
end
view 'ee/app/views/shared/_repository_size_limit_setting.html.haml' do view 'ee/app/views/shared/_repository_size_limit_setting.html.haml' do
element :repository_size_limit_field element :repository_size_limit_field
end end
...@@ -88,6 +92,18 @@ module QA ...@@ -88,6 +92,18 @@ module QA
click_element :save_permissions_changes_button click_element :save_permissions_changes_button
end end
def set_prevent_forking_outside_group_enabled
expand_content :permission_lfs_2fa_content
check_element :prevent_forking_outside_group_checkbox
click_element :save_permissions_changes_button
end
def set_prevent_forking_outside_group_disabled
expand_content :permission_lfs_2fa_content
uncheck_element :prevent_forking_outside_group_checkbox
click_element :save_permissions_changes_button
end
def set_repository_size_limit(limit) def set_repository_size_limit(limit)
find_element(:repository_size_limit_field).set limit find_element(:repository_size_limit_field).set limit
end end
......
...@@ -228,7 +228,7 @@ module QA ...@@ -228,7 +228,7 @@ module QA
def finished_loading_block? def finished_loading_block?
wait_for_requests wait_for_requests
has_no_css?('.fa-spinner.block-loading', wait: Capybara.default_max_wait_time) has_no_css?('.gl-spinner', wait: Capybara.default_max_wait_time)
end end
def has_loaded_all_images? def has_loaded_all_images?
......
...@@ -9,9 +9,17 @@ module QA ...@@ -9,9 +9,17 @@ module QA
element :fork_namespace_button element :fork_namespace_button
end end
view 'app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue' do
element :fork_groups_list_search_field
end
def choose_namespace(namespace = Runtime::Namespace.path) def choose_namespace(namespace = Runtime::Namespace.path)
click_element(:fork_namespace_button, name: namespace) click_element(:fork_namespace_button, name: namespace)
end end
def search_for_group(group_name)
find_element(:fork_groups_list_search_field).set(group_name)
end
end end
end end
end end
......
...@@ -76,6 +76,19 @@ module QA ...@@ -76,6 +76,19 @@ module QA
visibility: 'public' visibility: 'public'
} }
end end
def api_put_path
"/groups/#{id}"
end
def update_group_setting(group_setting:, value:)
put_body = { "#{group_setting}": value }
response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body
unless response.code == HTTP_STATUS_OK
raise ResourceUpdateFailedError, "Could not update #{group_setting} to #{value}. Request returned (#{response.code}): `#{response}`."
end
end
end end
end end
end end
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
require 'faker' require 'faker'
module QA module QA
RSpec.describe 'Verify', :runner, :requires_admin, :skip_live_env do RSpec.describe 'Verify', :runner do
describe "Include multiple files from a project" do describe 'Include multiple files from a project' do
let(:feature_flag) { :ci_include_multiple_files_from_project }
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" } let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
let(:expected_text) { Faker::Lorem.sentence } let(:expected_text) { Faker::Lorem.sentence }
let(:unexpected_text) { Faker::Lorem.sentence } let(:unexpected_text) { Faker::Lorem.sentence }
...@@ -31,7 +30,6 @@ module QA ...@@ -31,7 +30,6 @@ module QA
end end
before do before do
Runtime::Feature.enable(feature_flag)
Flow::Login.sign_in Flow::Login.sign_in
add_included_files add_included_files
add_main_ci_file add_main_ci_file
...@@ -40,7 +38,6 @@ module QA ...@@ -40,7 +38,6 @@ module QA
end end
after do after do
Runtime::Feature.disable(feature_flag)
runner.remove_via_api! runner.remove_via_api!
end end
......
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
require 'faker' require 'faker'
module QA module QA
RSpec.describe 'Verify', :runner, :requires_admin do RSpec.describe 'Verify', :runner do
describe "Pass dotenv variables to downstream via bridge" do describe 'Pass dotenv variables to downstream via bridge' do
let(:feature_flag) { :ci_bridge_dependency_variables }
let(:executor_1) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" } let(:executor_1) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
let(:executor_2) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" } let(:executor_2) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
...@@ -38,7 +37,6 @@ module QA ...@@ -38,7 +37,6 @@ module QA
end end
before do before do
Runtime::Feature.enable(feature_flag)
Flow::Login.sign_in Flow::Login.sign_in
add_ci_file(downstream_project, downstream_ci_file) add_ci_file(downstream_project, downstream_ci_file)
add_ci_file(upstream_project, upstream_ci_file) add_ci_file(upstream_project, upstream_ci_file)
...@@ -47,7 +45,6 @@ module QA ...@@ -47,7 +45,6 @@ module QA
end end
after do after do
Runtime::Feature.disable(feature_flag)
runner_1.remove_via_api! runner_1.remove_via_api!
runner_2.remove_via_api! runner_2.remove_via_api!
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module QA module QA
RSpec.describe 'Verify' do RSpec.describe 'Verify' do
describe 'Run pipeline', only: { subdomain: :staging } do describe 'Run pipeline' do
context 'with web only rule' do context 'with web only rule' do
let(:job_name) { 'test_job' } let(:job_name) { 'test_job' }
let(:project) do let(:project) do
......
...@@ -3,11 +3,8 @@ ...@@ -3,11 +3,8 @@
require 'faker' require 'faker'
module QA module QA
RSpec.describe 'Verify', :runner, :requires_admin do RSpec.describe 'Verify', :runner do
# [TODO]: Developer to remove :requires_admin once FF is removed in follow up issue
describe "Trigger child pipeline with 'when:manual'" do describe "Trigger child pipeline with 'when:manual'" do
let(:feature_flag) { :ci_manual_bridges } # [TODO]: Developer to remove when feature flag is removed
let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" } let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" }
let(:project) do let(:project) do
...@@ -25,7 +22,6 @@ module QA ...@@ -25,7 +22,6 @@ module QA
end end
before do before do
Runtime::Feature.enable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
Flow::Login.sign_in Flow::Login.sign_in
add_ci_files add_ci_files
project.visit! project.visit!
...@@ -33,7 +29,6 @@ module QA ...@@ -33,7 +29,6 @@ module QA
end end
after do after do
Runtime::Feature.disable(feature_flag) # [TODO]: Developer to remove when feature flag is removed
runner.remove_via_api! runner.remove_via_api!
end end
......
# frozen_string_literal: true
module QA
RSpec.describe 'Manage' do
describe 'prevent forking outside group' do
let!(:group_for_fork) do
Resource::Sandbox.fabricate_via_api! do |sandbox_group|
sandbox_group.path = "group_for_fork_#{SecureRandom.hex(8)}"
end
end
let(:project) do
Resource::Project.fabricate! do |project|
project.name = "project_to_fork"
project.initialize_with_readme = true
end
end
context 'when disabled' do
before do
set_prevent_forking_outside_group('disabled')
end
it 'allows forking outside of group', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1070' do
visit_project_and_search_group_for_fork
expect(page).to have_text(group_for_fork.path)
expect(page).to have_text('Select a namespace to fork the project')
end
end
context 'when enabled' do
before do
set_prevent_forking_outside_group('enabled')
end
it 'does not allow forking outside of group', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1107' do
visit_project_and_search_group_for_fork
expect(page).not_to have_text(group_for_fork.path)
expect(page).not_to have_text('Select a namespace to fork the project')
end
end
after do
project.group.sandbox.update_group_setting(group_setting: 'prevent_forking_outside_group', value: false)
project.remove_via_api!
group_for_fork.remove_via_api!
end
def set_prevent_forking_outside_group(enabled_or_disabled)
Flow::Login.sign_in
project.group.sandbox.visit!
Page::Group::Menu.perform(&:click_group_general_settings_item)
Page::Group::Settings::General.perform do |general_setting|
general_setting.send("set_prevent_forking_outside_group_#{enabled_or_disabled}")
end
end
def visit_project_and_search_group_for_fork
project.visit!
Page::Project::Show.perform(&:fork_project)
Page::Project::Fork::New.perform do |fork_new|
fork_new.search_for_group(group_for_fork.path)
end
end
end
end
end
...@@ -25,7 +25,7 @@ module QA ...@@ -25,7 +25,7 @@ module QA
# https://gitlab.com/groups/gitlab-org/-/epics/956 # https://gitlab.com/groups/gitlab-org/-/epics/956
# retry_on_exception added here due to `StaleElementReferenceError`. See: https://gitlab.com/gitlab-org/gitlab/-/issues/232485 # retry_on_exception added here due to `StaleElementReferenceError`. See: https://gitlab.com/gitlab-org/gitlab/-/issues/232485
Support::Retrier.retry_on_exception do Support::Retrier.retry_on_exception do
Capybara.page.has_no_css?('.gl-spinner, .fa-spinner, .spinner', wait: wait) Capybara.page.has_no_css?('.gl-spinner', wait: wait)
end end
end end
end end
......
...@@ -20,6 +20,7 @@ class JobFinder ...@@ -20,6 +20,7 @@ class JobFinder
@job_query = options.delete(:job_query) @job_query = options.delete(:job_query)
@pipeline_id = options.delete(:pipeline_id) @pipeline_id = options.delete(:pipeline_id)
@job_name = options.delete(:job_name) @job_name = options.delete(:job_name)
@artifact_path = options.delete(:artifact_path)
# Force the token to be a string so that if api_token is nil, it's set to '', allowing unauthenticated requests (for forks). # Force the token to be a string so that if api_token is nil, it's set to '', allowing unauthenticated requests (for forks).
api_token = options.delete(:api_token).to_s api_token = options.delete(:api_token).to_s
...@@ -33,19 +34,31 @@ class JobFinder ...@@ -33,19 +34,31 @@ class JobFinder
end end
def execute def execute
find_job_with_filtered_pipelines || find_job_in_pipeline find_job_with_artifact || find_job_with_filtered_pipelines || find_job_in_pipeline
end end
private private
attr_reader :project, :pipeline_query, :job_query, :pipeline_id, :job_name attr_reader :project, :pipeline_query, :job_query, :pipeline_id, :job_name, :artifact_path
def find_job_with_artifact
return if artifact_path.nil?
Gitlab.pipelines(project, pipeline_query_params).auto_paginate do |pipeline|
Gitlab.pipeline_jobs(project, pipeline.id, job_query_params).auto_paginate do |job|
return job if found_job_with_artifact?(job) # rubocop:disable Cop/AvoidReturnFromBlocks
end
end
raise 'Job not found!'
end
def find_job_with_filtered_pipelines def find_job_with_filtered_pipelines
return if pipeline_query.empty? return if pipeline_query.empty?
Gitlab.pipelines(project, pipeline_query_params).auto_paginate do |pipeline| Gitlab.pipelines(project, pipeline_query_params).auto_paginate do |pipeline|
Gitlab.pipeline_jobs(project, pipeline.id, job_query_params).auto_paginate do |job| Gitlab.pipeline_jobs(project, pipeline.id, job_query_params).auto_paginate do |job|
return job if job.name == job_name # rubocop:disable Cop/AvoidReturnFromBlocks return job if found_job_by_name?(job) # rubocop:disable Cop/AvoidReturnFromBlocks
end end
end end
...@@ -56,12 +69,22 @@ class JobFinder ...@@ -56,12 +69,22 @@ class JobFinder
return unless pipeline_id return unless pipeline_id
Gitlab.pipeline_jobs(project, pipeline_id, job_query_params).auto_paginate do |job| Gitlab.pipeline_jobs(project, pipeline_id, job_query_params).auto_paginate do |job|
return job if job.name == job_name # rubocop:disable Cop/AvoidReturnFromBlocks return job if found_job_by_name?(job) # rubocop:disable Cop/AvoidReturnFromBlocks
end end
raise 'Job not found!' raise 'Job not found!'
end end
def found_job_with_artifact?(job)
artifact_url = "https://gitlab.com/api/v4/projects/#{CGI.escape(project)}/jobs/#{job.id}/artifacts/#{artifact_path}"
response = HTTParty.head(artifact_url) # rubocop:disable Gitlab/HTTParty
response.success?
end
def found_job_by_name?(job)
job.name == job_name
end
def pipeline_query_params def pipeline_query_params
@pipeline_query_params ||= { per_page: 100, **pipeline_query } @pipeline_query_params ||= { per_page: 100, **pipeline_query }
end end
...@@ -95,6 +118,10 @@ if $0 == __FILE__ ...@@ -95,6 +118,10 @@ if $0 == __FILE__
options[:job_name] = value options[:job_name] = value
end end
opts.on("-a", "--artifact-path ARTIFACT_PATH", String, "A valid artifact path") do |value|
options[:artifact_path] = value
end
opts.on("-t", "--api-token API_TOKEN", String, "A value API token with the `read_api` scope") do |value| opts.on("-t", "--api-token API_TOKEN", String, "A value API token with the `read_api` scope") do |value|
options[:api_token] = value options[:api_token] = value
end end
......
#!/usr/bin/env bash #!/usr/bin/env bash
function retrieve_tests_metadata() { function retrieve_tests_metadata() {
mkdir -p crystalball/ knapsack/ rspec_flaky/ rspec_profiling/ mkdir -p knapsack/ rspec_flaky/ rspec_profiling/
local project_path="gitlab-org/gitlab" local project_path="gitlab-org/gitlab"
local test_metadata_job_id local test_metadata_job_id
...@@ -16,13 +16,6 @@ function retrieve_tests_metadata() { ...@@ -16,13 +16,6 @@ function retrieve_tests_metadata() {
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
scripts/api/download_job_artifact --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}" scripts/api/download_job_artifact --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
fi fi
# FIXME: We will need to find a pipeline where the $RSPEC_PACKED_TESTS_MAPPING_PATH.gz actually exists (Crystalball only runs every two-hours, but the `update-tests-metadata` runs for all `master` pipelines...).
# if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
# (scripts/api/download_job_artifact --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
# fi
#
# scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}"
} }
function update_tests_metadata() { function update_tests_metadata() {
...@@ -43,6 +36,21 @@ function update_tests_metadata() { ...@@ -43,6 +36,21 @@ function update_tests_metadata() {
fi fi
} }
function retrieve_tests_mapping() {
mkdir -p crystalball/
local project_path="gitlab-org/gitlab"
local test_metadata_with_mapping_job_id
test_metadata_with_mapping_job_id=$(scripts/api/get_job_id --project "${project_path}" -q "status=success" -q "ref=master" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-tests-metadata" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz")
if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
(scripts/api/download_job_artifact --project "${project_path}" --job-id "${test_metadata_with_mapping_job_id}" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
fi
scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}"
}
function update_tests_mapping() { function update_tests_mapping() {
if ! crystalball_rspec_data_exists; then if ! crystalball_rspec_data_exists; then
echo "No crystalball rspec data found." echo "No crystalball rspec data found."
...@@ -119,8 +127,8 @@ function rspec_paralellized_job() { ...@@ -119,8 +127,8 @@ function rspec_paralellized_job() {
local rspec_args="-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}" local rspec_args="-Ispec -rspec_helper --color --format documentation --format RspecJunitFormatter --out junit_rspec.xml ${rspec_opts}"
if [[ -n $RSPEC_MATCHING_TESTS_ENABLED ]]; then if [[ -n $RSPEC_TESTS_MAPPING_ENABLED ]]; then
tooling/bin/parallel_rspec --rspec_args "${rspec_args}" --filter tmp/matching_tests.txt tooling/bin/parallel_rspec --rspec_args "${rspec_args}" --filter "tmp/matching_tests.txt"
else else
tooling/bin/parallel_rspec --rspec_args "${rspec_args}" tooling/bin/parallel_rspec --rspec_args "${rspec_args}"
fi fi
......
...@@ -36,7 +36,7 @@ function install_gitlab_gem() { ...@@ -36,7 +36,7 @@ function install_gitlab_gem() {
} }
function install_tff_gem() { function install_tff_gem() {
gem install test_file_finder --version 0.1.0 gem install test_file_finder --version 0.1.1
} }
function run_timed_command() { function run_timed_command() {
......
...@@ -15,6 +15,54 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do ...@@ -15,6 +15,54 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end end
describe 'GET index' do describe 'GET index' do
describe 'pushing tracking_data to Gon' do
before do
stub_experiment(jobs_empty_state: experiment_active)
stub_experiment_for_user(jobs_empty_state: in_experiment_group)
get_index
end
context 'when experiment not active' do
let(:experiment_active) { false }
let(:in_experiment_group) { false }
it 'does not push tracking_data to Gon' do
expect(Gon.tracking_data).to be_nil
end
end
context 'when experiment active and user in control group' do
let(:experiment_active) { true }
let(:in_experiment_group) { false }
it 'pushes tracking_data to Gon' do
expect(Gon.tracking_data).to match(
{
category: 'Growth::Activation::Experiment::JobsEmptyState',
action: 'click_button',
label: anything,
property: 'control_group'
}
)
end
end
context 'when experiment active and user in experimental group' do
let(:experiment_active) { true }
let(:in_experiment_group) { true }
it 'pushes tracking_data to gon' do
expect(Gon.tracking_data).to match(
category: 'Growth::Activation::Experiment::JobsEmptyState',
action: 'click_button',
label: anything,
property: 'experimental_group'
)
end
end
end
context 'when scope is pending' do context 'when scope is pending' do
before do before do
create(:ci_build, :pending, pipeline: pipeline) create(:ci_build, :pending, pipeline: pipeline)
......
...@@ -6,4 +6,11 @@ FactoryBot.define do ...@@ -6,4 +6,11 @@ FactoryBot.define do
file { fixture_file_upload('spec/fixtures/dependency_proxy/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz') } file { fixture_file_upload('spec/fixtures/dependency_proxy/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz') }
file_name { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz' } file_name { 'a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4.gz' }
end end
factory :dependency_proxy_manifest, class: 'DependencyProxy::Manifest' do
group
file { fixture_file_upload('spec/fixtures/dependency_proxy/manifest') }
digest { 'sha256:5ab5a6872b264fe4fd35d63991b9b7d8425f4bc79e7cf4d563c10956581170c9' }
file_name { 'manifest' }
end
end end
...@@ -25,72 +25,113 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do ...@@ -25,72 +25,113 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
end end
describe "GET /:project/jobs" do describe "GET /:project/jobs" do
let!(:job) { create(:ci_build, pipeline: pipeline) } context 'with no jobs' do
context "Pending scope" do
before do before do
visit project_jobs_path(project, scope: :pending) stub_experiment(jobs_empty_state: experiment_active)
end stub_experiment_for_user(jobs_empty_state: in_experiment_group)
it "shows Pending tab jobs" do visit project_jobs_path(project)
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
expect(page).to have_content job.short_sha
expect(page).to have_content job.ref
expect(page).to have_content job.name
end end
end
context "Running scope" do context 'when experiment not active' do
before do let(:experiment_active) { false }
job.run! let(:in_experiment_group) { false }
visit project_jobs_path(project, scope: :running)
end
it "shows Running tab jobs" do it 'shows the empty state control page' do
expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_content('No jobs to show')
expect(page).to have_content job.short_sha expect(page).to have_link('Get started with Pipelines')
expect(page).to have_content job.ref end
expect(page).to have_content job.name
end end
end
context "Finished scope" do context 'when experiment active and user in control group' do
before do let(:experiment_active) { true }
job.run! let(:in_experiment_group) { false }
visit project_jobs_path(project, scope: :finished)
it 'shows the empty state control page' do
expect(page).to have_content('No jobs to show')
expect(page).to have_link('Get started with Pipelines')
end
end end
it "shows Finished tab jobs" do context 'when experiment active and user in experimental group' do
expect(page).to have_selector('.nav-links li.active', text: 'Finished') let(:experiment_active) { true }
expect(page).to have_content 'No jobs to show' let(:in_experiment_group) { true }
it 'shows the empty state experiment page' do
expect(page).to have_content('Use jobs to automate your tasks')
expect(page).to have_link('Create CI/CD configuration file')
end
end end
end end
context "All jobs" do context 'with a job' do
before do let!(:job) { create(:ci_build, pipeline: pipeline) }
project.builds.running_or_pending.each(&:success)
visit project_jobs_path(project) context "Pending scope" do
before do
visit project_jobs_path(project, scope: :pending)
end
it "shows Pending tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Pending')
expect(page).to have_content job.short_sha
expect(page).to have_content job.ref
expect(page).to have_content job.name
end
end end
it "shows All tab jobs" do context "Running scope" do
expect(page).to have_selector('.nav-links li.active', text: 'All') before do
expect(page).to have_content job.short_sha job.run!
expect(page).to have_content job.ref visit project_jobs_path(project, scope: :running)
expect(page).to have_content job.name end
it "shows Running tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Running')
expect(page).to have_content job.short_sha
expect(page).to have_content job.ref
expect(page).to have_content job.name
end
end end
end
context "when visiting old URL" do context "Finished scope" do
let(:jobs_url) do before do
project_jobs_path(project) job.run!
visit project_jobs_path(project, scope: :finished)
end
it "shows Finished tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'Finished')
expect(page).to have_content 'No jobs to show'
end
end end
before do context "All jobs" do
visit jobs_url.sub('/-/jobs', '/builds') before do
project.builds.running_or_pending.each(&:success)
visit project_jobs_path(project)
end
it "shows All tab jobs" do
expect(page).to have_selector('.nav-links li.active', text: 'All')
expect(page).to have_content job.short_sha
expect(page).to have_content job.ref
expect(page).to have_content job.name
end
end end
it "redirects to new URL" do context "when visiting old URL" do
expect(page.current_path).to eq(jobs_url) let(:jobs_url) do
project_jobs_path(project)
end
before do
visit jobs_url.sub('/-/jobs', '/builds')
end
it "redirects to new URL" do
expect(page.current_path).to eq(jobs_url)
end
end end
end end
end end
......
...@@ -19,7 +19,7 @@ RSpec.describe "User interacts with deploy keys", :js do ...@@ -19,7 +19,7 @@ RSpec.describe "User interacts with deploy keys", :js do
click_button("Enable") click_button("Enable")
expect(page).not_to have_selector(".fa-spinner") expect(page).not_to have_selector(".gl-spinner")
expect(current_path).to eq(project_settings_repository_path(project)) expect(current_path).to eq(project_settings_repository_path(project))
find(".js-deployKeys-tab-enabled_keys").click find(".js-deployKeys-tab-enabled_keys").click
......
{
"schemaVersion": 1,
"name": "library/alpine",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:188c0c94c7c576fff0792aca7ec73d67a2f7f4cb3a6e53a84559337260b36964"
}
],
"history": [
{
"v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\"],\"ArgsEscaped\":true,\"Image\":\"sha256:3543079adc6fb5170279692361be8b24e89ef1809a374c1b4429e1d560d1459c\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container\":\"8c59eb170e19b8c3768b8d06c91053b0debf4a6fa6a452df394145fe9b885ea5\",\"container_config\":{\"Hostname\":\"8c59eb170e19\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"CMD [\\\"/bin/sh\\\"]\"],\"ArgsEscaped\":true,\"Image\":\"sha256:3543079adc6fb5170279692361be8b24e89ef1809a374c1b4429e1d560d1459c\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"created\":\"2020-10-22T02:19:24.499382102Z\",\"docker_version\":\"18.09.7\",\"id\":\"c5f1aab5bb88eaf1aa62bea08ea6654547d43fd4d15b1a476c77e705dd5385ba\",\"os\":\"linux\",\"parent\":\"dc0b50cc52bc340d7848a62cfe8a756f4420592f4984f7a680ef8f9d258176ed\",\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"dc0b50cc52bc340d7848a62cfe8a756f4420592f4984f7a680ef8f9d258176ed\",\"created\":\"2020-10-22T02:19:24.33416307Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:f17f65714f703db9012f00e5ec98d0b2541ff6147c2633f7ab9ba659d0c507f4 in / \"]}}"
}
],
"signatures": [
{
"header": {
"jwk": {
"crv": "P-256",
"kid": "XOTE:DZ4C:YBPJ:3O3L:YI4B:NYXU:T4VR:USH6:CXXN:SELU:CSCC:FVPE",
"kty": "EC",
"x": "cR1zye_3354mdbD7Dn-mtXNXvtPtmLlUVDa5vH6Lp74",
"y": "rldUXSllLit6_2BW6AV8aqkwWJXHoYPG9OwkIBouwxQ"
},
"alg": "ES256"
},
"signature": "DYB2iB-XKIisqp5Q0OXFOBIOlBOuRV7pnZuKy0cxVB2Qj1VFRhWX4Tq336y0VMWbF6ma1he5A1E_Vk4jazrJ9g",
"protected": "eyJmb3JtYXRMZW5ndGgiOjIxMzcsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMC0xMS0yNFQyMjowMTo1MVoifQ"
}
]
}
\ No newline at end of file
<div class="js-create-item-dropdown-fixture-root"> <div class="js-create-item-dropdown-fixture-root">
<input name="variable[environment]" type="hidden"> <input name="variable[environment]" type="hidden" />
<div class="dropdown "><button class="dropdown-menu-toggle js-dropdown-menu-toggle" type="button" data-toggle="dropdown"><span class="dropdown-toggle-text ">some label</span><i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i></button><div class="dropdown-menu dropdown-select dropdown-menu-selectable"><div class="dropdown-input"><input type="search" id="" class="dropdown-input-field" autocomplete="off" /><i aria-hidden="true" data-hidden="true" class="fa fa-search dropdown-input-search"></i><i aria-hidden="true" data-hidden="true" role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i></div><div class="dropdown-content js-dropdown-content"></div><div class="dropdown-footer"><ul class="dropdown-footer-list"> <div class="dropdown ">
<li> <button
<button class="dropdown-create-new-item-button js-dropdown-create-new-item"> class="dropdown-menu-toggle js-dropdown-menu-toggle"
Create wildcard type="button"
<code></code> data-toggle="dropdown"
</button> >
</li> <span class="dropdown-toggle-text ">some label</span
</ul> ><i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
</div><div class="dropdown-loading"><i aria-hidden="true" data-hidden="true" class="fa fa-spinner fa-spin"></i></div></div></div></div> </button>
<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
<div class="dropdown-input">
<input type="search" id="" class="dropdown-input-field" autocomplete="off" /><i
aria-hidden="true"
data-hidden="true"
class="fa fa-search dropdown-input-search"
></i
><i
aria-hidden="true"
data-hidden="true"
role="button"
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
></i>
</div>
<div class="dropdown-content js-dropdown-content"></div>
<div class="dropdown-footer">
<ul class="dropdown-footer-list">
<li>
<button class="dropdown-create-new-item-button js-dropdown-create-new-item">
Create wildcard
<code></code>
</button>
</li>
</ul>
</div>
<div class="dropdown-loading">
<span aria-hidden="true" data-hidden="true" class="gl-spinner"></span>
</div>
</div>
</div>
</div>
...@@ -32,7 +32,7 @@ ...@@ -32,7 +32,7 @@
</div> </div>
<div class="dropdown-content"></div> <div class="dropdown-content"></div>
<div class="dropdown-loading"> <div class="dropdown-loading">
<i class="fa fa-spinner fa-spin"></i> <span class="gl-spinner"></span>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<ul></ul> <ul></ul>
</li> </li>
<li class="js-builds-dropdown-loading hidden"> <li class="js-builds-dropdown-loading hidden">
<span class="fa fa-spinner"></span> <span class="gl-spinner"></span>
</li> </li>
</ul> </ul>
</div> </div>
<div class="project-item-select-holder"> <div class="project-item-select-holder">
<input class="project-item-select" data-group-id="12345" data-relative-path="issues/new"> <input class="project-item-select" data-group-id="12345" data-relative-path="issues/new" />
<a class="new-project-item-link" data-label="New issue" data-type="issues" href=""> <a class="new-project-item-link" data-label="New issue" data-type="issues" href="">
<i class="fa fa-spinner spin"></i> <span class="gl-spinner"></span>
</a> </a>
<a class="new-project-item-select-button"> <a class="new-project-item-select-button">
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</a> </a>
</div> </div>
import Vue from 'vue'; import Vue from 'vue';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import { setHTMLFixture } from 'helpers/fixtures'; import { setHTMLFixture } from 'helpers/fixtures';
import PipelineStore from '~/pipelines/stores/pipeline_store'; import PipelineStore from '~/pipelines/stores/pipeline_store';
import GraphComponentLegacy from '~/pipelines/components/graph/graph_component_legacy.vue'; import GraphComponentLegacy from '~/pipelines/components/graph/graph_component_legacy.vue';
...@@ -42,7 +43,7 @@ describe('graph component', () => { ...@@ -42,7 +43,7 @@ describe('graph component', () => {
}, },
}); });
expect(wrapper.find('.gl-spinner').exists()).toBe(true); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
}); });
}); });
...@@ -85,7 +86,7 @@ describe('graph component', () => { ...@@ -85,7 +86,7 @@ describe('graph component', () => {
}); });
it('should not include the loading icon', () => { it('should not include the loading icon', () => {
expect(wrapper.find('.fa-spinner').exists()).toBe(false); expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
}); });
it('should include the stage column', () => { it('should include the stage column', () => {
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DependencyProxy::Manifest, type: :model do
describe 'relationships' do
it { is_expected.to belong_to(:group) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:group) }
it { is_expected.to validate_presence_of(:file) }
it { is_expected.to validate_presence_of(:file_name) }
it { is_expected.to validate_presence_of(:digest) }
end
describe 'file is being stored' do
subject { create(:dependency_proxy_manifest) }
context 'when existing object has local store' do
it_behaves_like 'mounted file in local store'
end
context 'when direct upload is enabled' do
before do
stub_dependency_proxy_object_storage(direct_upload: true)
end
it_behaves_like 'mounted file in object store'
end
end
end
...@@ -51,7 +51,7 @@ RSpec.describe ExclusiveLeaseGuard, :clean_gitlab_redis_shared_state do ...@@ -51,7 +51,7 @@ RSpec.describe ExclusiveLeaseGuard, :clean_gitlab_redis_shared_state do
it 'does not call internal_method but logs error', :aggregate_failures do it 'does not call internal_method but logs error', :aggregate_failures do
expect(subject).not_to receive(:internal_method) expect(subject).not_to receive(:internal_method)
expect(Gitlab::AppLogger).to receive(:error).with("Cannot obtain an exclusive lease for #{subject.class.name}. There must be another instance already in execution.") expect(Gitlab::AppLogger).to receive(:error).with("Cannot obtain an exclusive lease for #{subject.lease_key}. There must be another instance already in execution.")
subject.call subject.call
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::Pages::LegacyStorageLease do
let(:project) { create(:project) }
let(:implementation) do
Class.new do
include ::Pages::LegacyStorageLease
attr_reader :project
def initialize(project)
@project = project
end
def execute
try_obtain_lease do
execute_unsafe
end
end
def execute_unsafe
true
end
end
end
let(:service) { implementation.new(project) }
it 'allows method to be executed' do
expect(service).to receive(:execute_unsafe).and_call_original
expect(service.execute).to eq(true)
end
context 'when another service holds the lease for the same project' do
around do |example|
implementation.new(project).try_obtain_lease do
example.run
end
end
it 'does not run guarded method' do
expect(service).not_to receive(:execute_unsafe)
expect(service.execute).to eq(nil)
end
it 'runs guarded method if feature flag is disabled' do
stub_feature_flags(pages_use_legacy_storage_lease: false)
expect(service).to receive(:execute_unsafe).and_call_original
expect(service.execute).to eq(true)
end
end
context 'when another service holds the lease for the different project' do
around do |example|
implementation.new(create(:project)).try_obtain_lease do
example.run
end
end
it 'allows method to be executed' do
expect(service).to receive(:execute_unsafe).and_call_original
expect(service.execute).to eq(true)
end
end
end
...@@ -139,7 +139,7 @@ RSpec.describe Projects::GitDeduplicationService do ...@@ -139,7 +139,7 @@ RSpec.describe Projects::GitDeduplicationService do
end end
it 'fails when a lease is already out' do it 'fails when a lease is already out' do
expect(service).to receive(:log_error).with("Cannot obtain an exclusive lease for #{service.class.name}. There must be another instance already in execution.") expect(service).to receive(:log_error).with("Cannot obtain an exclusive lease for #{lease_key}. There must be another instance already in execution.")
service.execute service.execute
end end
......
...@@ -69,6 +69,16 @@ RSpec.describe Projects::UpdatePagesService do ...@@ -69,6 +69,16 @@ RSpec.describe Projects::UpdatePagesService do
expect(project.pages_metadatum.reload.pages_deployment_id).to eq(deployment.id) expect(project.pages_metadatum.reload.pages_deployment_id).to eq(deployment.id)
end end
it 'fails if another deployment is in progress' do
subject.try_obtain_lease do
expect do
execute
end.to raise_error("Failed to deploy pages - other deployment is in progress")
expect(GenericCommitStatus.last.description).to eq("Failed to deploy pages - other deployment is in progress")
end
end
it 'does not fail if pages_metadata is absent' do it 'does not fail if pages_metadata is absent' do
project.pages_metadatum.destroy! project.pages_metadatum.destroy!
project.reload project.reload
......
...@@ -7,7 +7,8 @@ module Spec ...@@ -7,7 +7,8 @@ module Spec
module MergeRequestHelpers module MergeRequestHelpers
def preload_view_requirements(merge_request, note) def preload_view_requirements(merge_request, note)
# This will load the status fields of the author of the note and merge request # This will load the status fields of the author of the note and merge request
# to avoid queries in when rendering the view being tested. # to avoid queries when rendering the view being tested.
#
merge_request.author.status merge_request.author.status
note.author.status note.author.status
end end
......
...@@ -22,7 +22,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do ...@@ -22,7 +22,7 @@ RSpec.describe 'projects/commits/_commit.html.haml' do
} }
within '.gpg-status-box' do within '.gpg-status-box' do
expect(page).not_to have_css('i.fa.fa-spinner.fa-spin') expect(page).not_to have_css('.gl-spinner')
end end
end end
end end
......
...@@ -32,7 +32,7 @@ RSpec.describe RepositoryRemoveRemoteWorker do ...@@ -32,7 +32,7 @@ RSpec.describe RepositoryRemoveRemoteWorker do
expect(subject) expect(subject)
.to receive(:log_error) .to receive(:log_error)
.with("Cannot obtain an exclusive lease for #{subject.class.name}. There must be another instance already in execution.") .with("Cannot obtain an exclusive lease for #{lease_key}. There must be another instance already in execution.")
subject.perform(project.id, remote_name) subject.perform(project.id, remote_name)
end end
......
...@@ -3,9 +3,8 @@ ...@@ -3,9 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_state do RSpec.describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_state do
subject { described_class.new } let_it_be(:remote_mirror) { create(:remote_mirror) }
let(:remote_mirror) { create(:remote_mirror) }
let(:scheduled_time) { Time.current - 5.minutes } let(:scheduled_time) { Time.current - 5.minutes }
around do |example| around do |example|
...@@ -19,6 +18,8 @@ RSpec.describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_st ...@@ -19,6 +18,8 @@ RSpec.describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_st
end end
describe '#perform' do describe '#perform' do
subject { described_class.new }
it 'calls out to the service to perform the update' do it 'calls out to the service to perform the update' do
expect_mirror_service_to_return(remote_mirror, status: :success) expect_mirror_service_to_return(remote_mirror, status: :success)
...@@ -68,4 +69,8 @@ RSpec.describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_st ...@@ -68,4 +69,8 @@ RSpec.describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_st
subject.perform(remote_mirror.id, scheduled_time) subject.perform(remote_mirror.id, scheduled_time)
end end
end end
include_examples 'an idempotent worker' do
let(:job_args) { [remote_mirror.id, scheduled_time] }
end
end end
...@@ -19,7 +19,12 @@ mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID') ...@@ -19,7 +19,12 @@ mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID')
mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid) mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid)
changed_files = mr_changes.changes.map { |change| change['new_path'] } changed_files = mr_changes.changes.map { |change| change['new_path'] }
mapping = TestFileFinder::Mapping.load('tests.yml') tff = TestFileFinder::FileFinder.new(paths: changed_files).tap do |file_finder|
test_files = TestFileFinder::FileFinder.new(paths: changed_files, mapping: mapping).test_files file_finder.use TestFileFinder::MappingStrategies::PatternMatching.load('tests.yml')
File.write(output_file, test_files.uniq.join(' ')) if ENV['RSPEC_TESTS_MAPPING_ENABLED']
file_finder.use TestFileFinder::MappingStrategies::DirectMatching.load_json(ENV['RSPEC_TESTS_MAPPING_PATH'])
end
end
File.write(output_file, tff.test_files.uniq.join(' '))
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