Add latest changes from gitlab-org/gitlab@master
|
@ -28,6 +28,12 @@ AllCops:
|
|||
- 'file_hooks/**/*'
|
||||
CacheRootDirectory: tmp
|
||||
|
||||
Cop/AvoidKeywordArgumentsInSidekiqWorkers:
|
||||
Enabled: true
|
||||
Include:
|
||||
- 'app/workers/**/*'
|
||||
- 'ee/app/workers/**/*'
|
||||
|
||||
Cop/StaticTranslationDefinition:
|
||||
Enabled: true
|
||||
Exclude:
|
||||
|
|
2
Gemfile
|
@ -481,8 +481,6 @@ gem 'countries', '~> 3.0'
|
|||
|
||||
gem 'retriable', '~> 3.1.2'
|
||||
|
||||
gem 'liquid', '~> 4.0'
|
||||
|
||||
# LRU cache
|
||||
gem 'lru_redux'
|
||||
|
||||
|
|
|
@ -602,7 +602,6 @@ GEM
|
|||
xml-simple
|
||||
licensee (8.9.2)
|
||||
rugged (~> 0.24)
|
||||
liquid (4.0.3)
|
||||
listen (3.1.5)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
|
@ -1289,7 +1288,6 @@ DEPENDENCIES
|
|||
letter_opener_web (~> 1.3.4)
|
||||
license_finder (~> 5.4)
|
||||
licensee (~> 8.9)
|
||||
liquid (~> 4.0)
|
||||
lockbox (~> 0.3.3)
|
||||
lograge (~> 0.5)
|
||||
loofah (~> 2.2)
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { isString } from 'lodash';
|
||||
import { VARIABLE_TYPES } from '../constants';
|
||||
|
||||
/**
|
||||
* This file exclusively deals with parsing user-defined variables
|
||||
* in dashboard yml file.
|
||||
*
|
||||
* As of 13.0, simple custom and advanced custom variables are supported.
|
||||
* As of 13.0, simple text, advanced text, simple custom and
|
||||
* advanced custom variables are supported.
|
||||
*
|
||||
* In the future iterations, text and query variables will be
|
||||
* supported
|
||||
|
@ -12,13 +14,30 @@ import { VARIABLE_TYPES } from '../constants';
|
|||
*/
|
||||
|
||||
/**
|
||||
* Utility method to determine if a custom variable is
|
||||
* simple or not. If its not simple, it is advanced.
|
||||
* Simple text variable is a string value only.
|
||||
* This method parses such variables to a standard format.
|
||||
*
|
||||
* @param {Array|Object} customVar Array if simple, object if advanced
|
||||
* @returns {Boolean} true if simple, false if advanced
|
||||
* @param {String|Object} simpleTextVar
|
||||
* @returns {Object}
|
||||
*/
|
||||
const isSimpleCustomVariable = customVar => Array.isArray(customVar);
|
||||
const textSimpleVariableParser = simpleTextVar => ({
|
||||
type: VARIABLE_TYPES.text,
|
||||
label: null,
|
||||
value: simpleTextVar,
|
||||
});
|
||||
|
||||
/**
|
||||
* Advanced text variable is an object.
|
||||
* This method parses such variables to a standard format.
|
||||
*
|
||||
* @param {Object} advTextVar
|
||||
* @returns {Object}
|
||||
*/
|
||||
const textAdvancedVariableParser = advTextVar => ({
|
||||
type: VARIABLE_TYPES.text,
|
||||
label: advTextVar.label,
|
||||
value: advTextVar.options.default_value,
|
||||
});
|
||||
|
||||
/**
|
||||
* Normalize simple and advanced custom variable options to a standard
|
||||
|
@ -26,20 +45,12 @@ const isSimpleCustomVariable = customVar => Array.isArray(customVar);
|
|||
* @param {Object} custom variable option
|
||||
* @returns {Object} normalized custom variable options
|
||||
*/
|
||||
const normalizeDropdownOptions = ({ default: defaultOpt = false, text, value }) => ({
|
||||
const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, value }) => ({
|
||||
default: defaultOpt,
|
||||
text,
|
||||
value,
|
||||
});
|
||||
|
||||
/**
|
||||
* Simple custom variables have an array of values.
|
||||
* This method parses such variables options to a standard format.
|
||||
*
|
||||
* @param {String} opt option from simple custom variable
|
||||
*/
|
||||
const parseSimpleDropdownOptions = opt => ({ text: opt, value: opt });
|
||||
|
||||
/**
|
||||
* Custom advanced variables are rendered as dropdown elements in the dashboard
|
||||
* header. This method parses advanced custom variables.
|
||||
|
@ -52,10 +63,19 @@ const customAdvancedVariableParser = advVariable => {
|
|||
return {
|
||||
type: VARIABLE_TYPES.custom,
|
||||
label: advVariable.label,
|
||||
options: options.map(normalizeDropdownOptions),
|
||||
options: options.map(normalizeCustomVariableOptions),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple custom variables have an array of values.
|
||||
* This method parses such variables options to a standard format.
|
||||
*
|
||||
* @param {String} opt option from simple custom variable
|
||||
* @returns {Object}
|
||||
*/
|
||||
const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
|
||||
|
||||
/**
|
||||
* Custom simple variables are rendered as dropdown elements in the dashboard
|
||||
* header. This method parses simple custom variables.
|
||||
|
@ -66,14 +86,23 @@ const customAdvancedVariableParser = advVariable => {
|
|||
* @returns {Object}
|
||||
*/
|
||||
const customSimpleVariableParser = simpleVar => {
|
||||
const options = (simpleVar || []).map(parseSimpleDropdownOptions);
|
||||
const options = (simpleVar || []).map(parseSimpleCustomOptions);
|
||||
return {
|
||||
type: VARIABLE_TYPES.custom,
|
||||
label: null,
|
||||
options: options.map(normalizeDropdownOptions),
|
||||
options: options.map(normalizeCustomVariableOptions),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility method to determine if a custom variable is
|
||||
* simple or not. If its not simple, it is advanced.
|
||||
*
|
||||
* @param {Array|Object} customVar Array if simple, object if advanced
|
||||
* @returns {Boolean} true if simple, false if advanced
|
||||
*/
|
||||
const isSimpleCustomVariable = customVar => Array.isArray(customVar);
|
||||
|
||||
/**
|
||||
* This method returns a parser based on the type of the variable.
|
||||
* Currently, the supported variables are simple custom and
|
||||
|
@ -88,6 +117,10 @@ const getVariableParser = variable => {
|
|||
return customSimpleVariableParser;
|
||||
} else if (variable.type === VARIABLE_TYPES.custom) {
|
||||
return customAdvancedVariableParser;
|
||||
} else if (variable.type === VARIABLE_TYPES.text) {
|
||||
return textAdvancedVariableParser;
|
||||
} else if (isString(variable)) {
|
||||
return textSimpleVariableParser;
|
||||
}
|
||||
return () => null;
|
||||
};
|
||||
|
|
|
@ -104,9 +104,15 @@ export default {
|
|||
},
|
||||
fields() {
|
||||
const tagClass = this.isDesktop ? 'w-25' : '';
|
||||
const tagInnerClass = this.isDesktop ? 'mw-m' : 'gl-justify-content-end';
|
||||
return [
|
||||
{ key: LIST_KEY_CHECKBOX, label: '', class: 'gl-w-16' },
|
||||
{ key: LIST_KEY_TAG, label: LIST_LABEL_TAG, class: `${tagClass} js-tag-column` },
|
||||
{
|
||||
key: LIST_KEY_TAG,
|
||||
label: LIST_LABEL_TAG,
|
||||
class: `${tagClass} js-tag-column`,
|
||||
innerClass: tagInnerClass,
|
||||
},
|
||||
{ key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID },
|
||||
{ key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE },
|
||||
{ key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED },
|
||||
|
@ -329,17 +335,24 @@ export default {
|
|||
@change="updateSelectedItems(index)"
|
||||
/>
|
||||
</template>
|
||||
<template #cell(name)="{item}">
|
||||
<span ref="rowName">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<clipboard-button
|
||||
v-if="item.location"
|
||||
ref="rowClipboardButton"
|
||||
:title="item.location"
|
||||
:text="item.location"
|
||||
css-class="btn-default btn-transparent btn-clipboard"
|
||||
/>
|
||||
<template #cell(name)="{item, field}">
|
||||
<div ref="rowName" :class="[field.innerClass, 'gl-display-flex']">
|
||||
<span
|
||||
v-gl-tooltip
|
||||
data-testid="rowNameText"
|
||||
:title="item.name"
|
||||
class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
|
||||
>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<clipboard-button
|
||||
v-if="item.location"
|
||||
ref="rowClipboardButton"
|
||||
:title="item.location"
|
||||
:text="item.location"
|
||||
css-class="btn-default btn-transparent btn-clipboard"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(short_revision)="{value}">
|
||||
<span ref="rowShortRevision">
|
||||
|
|
|
@ -13,11 +13,7 @@ export default {
|
|||
IssuesList,
|
||||
},
|
||||
props: {
|
||||
baseEndpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
headEndpoint: {
|
||||
endpoint: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
|
@ -34,15 +30,12 @@ export default {
|
|||
]),
|
||||
},
|
||||
created() {
|
||||
this.setEndpoints({
|
||||
baseEndpoint: this.baseEndpoint,
|
||||
headEndpoint: this.headEndpoint,
|
||||
});
|
||||
this.setEndpoint(this.endpoint);
|
||||
|
||||
this.fetchReport();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['fetchReport', 'setEndpoints']),
|
||||
...mapActions(['fetchReport', 'setEndpoint']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,49 +1,78 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import Poll from '~/lib/utils/poll';
|
||||
import httpStatusCodes from '~/lib/utils/http_status';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import * as types from './mutation_types';
|
||||
import { parseAccessibilityReport, compareAccessibilityReports } from './utils';
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
export const setEndpoints = ({ commit }, { baseEndpoint, headEndpoint }) =>
|
||||
commit(types.SET_ENDPOINTS, { baseEndpoint, headEndpoint });
|
||||
let eTagPoll;
|
||||
|
||||
export const clearEtagPoll = () => {
|
||||
eTagPoll = null;
|
||||
};
|
||||
|
||||
export const stopPolling = () => {
|
||||
if (eTagPoll) eTagPoll.stop();
|
||||
};
|
||||
|
||||
export const restartPolling = () => {
|
||||
if (eTagPoll) eTagPoll.restart();
|
||||
};
|
||||
|
||||
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
|
||||
|
||||
/**
|
||||
* We need to poll the report endpoint while they are being parsed in the Backend.
|
||||
* This can take up to one minute.
|
||||
*
|
||||
* Poll.js will handle etag response.
|
||||
* While http status code is 204, it means it's parsing, and we'll keep polling
|
||||
* When http status code is 200, it means parsing is done, we can show the results & stop polling
|
||||
* When http status code is 500, it means parsing went wrong and we stop polling
|
||||
*/
|
||||
export const fetchReport = ({ state, dispatch, commit }) => {
|
||||
commit(types.REQUEST_REPORT);
|
||||
|
||||
// If we don't have both endpoints, throw an error.
|
||||
if (!state.baseEndpoint || !state.headEndpoint) {
|
||||
commit(
|
||||
types.RECEIVE_REPORT_ERROR,
|
||||
s__('AccessibilityReport|Accessibility report artifact not found'),
|
||||
);
|
||||
return;
|
||||
eTagPoll = new Poll({
|
||||
resource: {
|
||||
getReport(endpoint) {
|
||||
return axios.get(endpoint);
|
||||
},
|
||||
},
|
||||
data: state.endpoint,
|
||||
method: 'getReport',
|
||||
successCallback: ({ status, data }) => dispatch('receiveReportSuccess', { status, data }),
|
||||
errorCallback: () => dispatch('receiveReportError'),
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
eTagPoll.makeRequest();
|
||||
} else {
|
||||
axios
|
||||
.get(state.endpoint)
|
||||
.then(({ status, data }) => dispatch('receiveReportSuccess', { status, data }))
|
||||
.catch(() => dispatch('receiveReportError'));
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
axios.get(state.baseEndpoint).then(response => ({
|
||||
...response.data,
|
||||
isHead: false,
|
||||
})),
|
||||
axios.get(state.headEndpoint).then(response => ({
|
||||
...response.data,
|
||||
isHead: true,
|
||||
})),
|
||||
])
|
||||
.then(responses => dispatch('receiveReportSuccess', responses))
|
||||
.catch(() =>
|
||||
commit(
|
||||
types.RECEIVE_REPORT_ERROR,
|
||||
s__('AccessibilityReport|Failed to retrieve accessibility report'),
|
||||
),
|
||||
);
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden() && state.isLoading) {
|
||||
dispatch('restartPolling');
|
||||
} else {
|
||||
dispatch('stopPolling');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const receiveReportSuccess = ({ commit }, responses) => {
|
||||
const parsedReports = responses.map(response => ({
|
||||
isHead: response.isHead,
|
||||
issues: parseAccessibilityReport(response),
|
||||
}));
|
||||
const report = compareAccessibilityReports(parsedReports);
|
||||
commit(types.RECEIVE_REPORT_SUCCESS, report);
|
||||
export const receiveReportSuccess = ({ commit, dispatch }, { status, data }) => {
|
||||
if (status === httpStatusCodes.OK) {
|
||||
commit(types.RECEIVE_REPORT_SUCCESS, data);
|
||||
// Stop polling since we have the information already parsed and it won't be changing
|
||||
dispatch('stopPolling');
|
||||
}
|
||||
};
|
||||
|
||||
export const receiveReportError = ({ commit, dispatch }) => {
|
||||
commit(types.RECEIVE_REPORT_ERROR);
|
||||
dispatch('stopPolling');
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
|
|
|
@ -10,8 +10,7 @@ export const groupedSummaryText = state => {
|
|||
return s__('Reports|Accessibility scanning failed loading results');
|
||||
}
|
||||
|
||||
const numberOfResults =
|
||||
(state.report?.summary?.errors || 0) + (state.report?.summary?.warnings || 0);
|
||||
const numberOfResults = state.report?.summary?.errored || 0;
|
||||
if (numberOfResults === 0) {
|
||||
return s__('Reports|Accessibility scanning detected no issues for the source branch only');
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
|
||||
export const SET_ENDPOINT = 'SET_ENDPOINT';
|
||||
|
||||
export const REQUEST_REPORT = 'REQUEST_REPORT';
|
||||
export const RECEIVE_REPORT_SUCCESS = 'RECEIVE_REPORT_SUCCESS';
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import * as types from './mutation_types';
|
||||
|
||||
export default {
|
||||
[types.SET_ENDPOINTS](state, { baseEndpoint, headEndpoint }) {
|
||||
state.baseEndpoint = baseEndpoint;
|
||||
state.headEndpoint = headEndpoint;
|
||||
[types.SET_ENDPOINT](state, endpoint) {
|
||||
state.endpoint = endpoint;
|
||||
},
|
||||
[types.REQUEST_REPORT](state) {
|
||||
state.isLoading = true;
|
||||
|
@ -13,10 +12,9 @@ export default {
|
|||
state.isLoading = false;
|
||||
state.report = report;
|
||||
},
|
||||
[types.RECEIVE_REPORT_ERROR](state, message) {
|
||||
[types.RECEIVE_REPORT_ERROR](state) {
|
||||
state.isLoading = false;
|
||||
state.hasError = true;
|
||||
state.errorMessage = message;
|
||||
state.report = {};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
export default (initialState = {}) => ({
|
||||
baseEndpoint: initialState.baseEndpoint || '',
|
||||
headEndpoint: initialState.headEndpoint || '',
|
||||
endpoint: initialState.endpoint || '',
|
||||
|
||||
isLoading: initialState.isLoading || false,
|
||||
hasError: initialState.hasError || false,
|
||||
|
@ -11,9 +10,8 @@ export default (initialState = {}) => ({
|
|||
* status: {String},
|
||||
* summary: {
|
||||
* total: {Number},
|
||||
* notes: {Number},
|
||||
* warnings: {Number},
|
||||
* errors: {Number},
|
||||
* resolved: {Number},
|
||||
* errored: {Number},
|
||||
* },
|
||||
* existing_errors: {Array.<Object>},
|
||||
* existing_notes: {Array.<Object>},
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
import { difference, intersection } from 'lodash';
|
||||
import {
|
||||
STATUS_FAILED,
|
||||
STATUS_SUCCESS,
|
||||
ACCESSIBILITY_ISSUE_ERROR,
|
||||
ACCESSIBILITY_ISSUE_WARNING,
|
||||
} from '../../constants';
|
||||
|
||||
export const parseAccessibilityReport = data => {
|
||||
// Combine all issues into one array
|
||||
return Object.keys(data.results)
|
||||
.map(key => [...data.results[key]])
|
||||
.flat()
|
||||
.map(issue => JSON.stringify(issue)); // stringify to help with comparisons
|
||||
};
|
||||
|
||||
export const compareAccessibilityReports = reports => {
|
||||
const result = {
|
||||
status: '',
|
||||
summary: {
|
||||
total: 0,
|
||||
notes: 0,
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
},
|
||||
new_errors: [],
|
||||
new_notes: [],
|
||||
new_warnings: [],
|
||||
resolved_errors: [],
|
||||
resolved_notes: [],
|
||||
resolved_warnings: [],
|
||||
existing_errors: [],
|
||||
existing_notes: [],
|
||||
existing_warnings: [],
|
||||
};
|
||||
|
||||
const headReport = reports.filter(report => report.isHead)[0];
|
||||
const baseReport = reports.filter(report => !report.isHead)[0];
|
||||
|
||||
// existing issues are those that exist in both the head report and the base report
|
||||
const existingIssues = intersection(headReport.issues, baseReport.issues);
|
||||
// new issues are those that exist in only the head report
|
||||
const newIssues = difference(headReport.issues, baseReport.issues);
|
||||
// resolved issues are those that exist in only the base report
|
||||
const resolvedIssues = difference(baseReport.issues, headReport.issues);
|
||||
|
||||
const parseIssues = (issue, issueType, shouldCount) => {
|
||||
const parsedIssue = JSON.parse(issue);
|
||||
switch (parsedIssue.type) {
|
||||
case ACCESSIBILITY_ISSUE_ERROR:
|
||||
result[`${issueType}_errors`].push(parsedIssue);
|
||||
if (shouldCount) {
|
||||
result.summary.errors += 1;
|
||||
}
|
||||
break;
|
||||
case ACCESSIBILITY_ISSUE_WARNING:
|
||||
result[`${issueType}_warnings`].push(parsedIssue);
|
||||
if (shouldCount) {
|
||||
result.summary.warnings += 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
result[`${issueType}_notes`].push(parsedIssue);
|
||||
if (shouldCount) {
|
||||
result.summary.notes += 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
existingIssues.forEach(issue => parseIssues(issue, 'existing', true));
|
||||
newIssues.forEach(issue => parseIssues(issue, 'new', true));
|
||||
resolvedIssues.forEach(issue => parseIssues(issue, 'resolved', false));
|
||||
|
||||
result.summary.total = result.summary.errors + result.summary.warnings + result.summary.notes;
|
||||
const hasErrorsOrWarnings = result.summary.errors > 0 || result.summary.warnings > 0;
|
||||
result.status = hasErrorsOrWarnings ? STATUS_FAILED : STATUS_SUCCESS;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// prevent babel-plugin-rewire from generating an invalid default during karma tests
|
||||
export default () => {};
|
|
@ -146,11 +146,7 @@ export default {
|
|||
});
|
||||
},
|
||||
shouldShowAccessibilityReport() {
|
||||
return (
|
||||
this.accessibilility?.base_path &&
|
||||
this.accessibilility?.head_path &&
|
||||
this.glFeatures.accessibilityMergeRequestWidget
|
||||
);
|
||||
return this.mr.accessibilityReportPath && this.glFeatures.accessibilityMergeRequestWidget;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -396,8 +392,7 @@ export default {
|
|||
|
||||
<grouped-accessibility-reports-app
|
||||
v-if="shouldShowAccessibilityReport"
|
||||
:base-endpoint="mr.accessibility.base_path"
|
||||
:head-endpoint="mr.accessibility.head_path"
|
||||
:endpoint="mr.accessibilityReportPath"
|
||||
/>
|
||||
|
||||
<div class="mr-widget-section">
|
||||
|
|
|
@ -103,7 +103,7 @@ export default class MergeRequestStore {
|
|||
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
|
||||
this.terraformReportsPath = data.terraform_reports_path;
|
||||
this.testResultsPath = data.test_reports_path;
|
||||
this.accessibility = data.accessibility || {};
|
||||
this.accessibilityReportPath = data.accessibility_report_path;
|
||||
this.exposedArtifactsPath = data.exposed_artifacts_path;
|
||||
this.cancelAutoMergePath = data.cancel_auto_merge_path;
|
||||
this.canCancelAutomaticMerge = Boolean(data.cancel_auto_merge_path);
|
||||
|
|
|
@ -16,10 +16,10 @@ module Types
|
|||
field :panel_id, GraphQL::STRING_TYPE, null: true,
|
||||
description: 'ID of a dashboard panel to which the annotation should be scoped'
|
||||
|
||||
field :starting_at, GraphQL::STRING_TYPE, null: true,
|
||||
field :starting_at, Types::TimeType, null: true,
|
||||
description: 'Timestamp marking start of annotated time span'
|
||||
|
||||
field :ending_at, GraphQL::STRING_TYPE, null: true,
|
||||
field :ending_at, Types::TimeType, null: true,
|
||||
description: 'Timestamp marking end of annotated time span'
|
||||
|
||||
def panel_id
|
||||
|
|
|
@ -8,6 +8,7 @@ class SnippetRepository < ApplicationRecord
|
|||
|
||||
CommitError = Class.new(StandardError)
|
||||
InvalidPathError = Class.new(CommitError)
|
||||
InvalidSignatureError = Class.new(CommitError)
|
||||
|
||||
belongs_to :snippet, inverse_of: :snippet_repository
|
||||
|
||||
|
@ -41,8 +42,8 @@ class SnippetRepository < ApplicationRecord
|
|||
rescue Gitlab::Git::Index::IndexError,
|
||||
Gitlab::Git::CommitError,
|
||||
Gitlab::Git::PreReceiveError,
|
||||
Gitlab::Git::CommandError => error
|
||||
|
||||
Gitlab::Git::CommandError,
|
||||
ArgumentError => error
|
||||
raise commit_error_exception(error)
|
||||
end
|
||||
|
||||
|
@ -88,15 +89,23 @@ class SnippetRepository < ApplicationRecord
|
|||
"#{DEFAULT_EMPTY_FILE_NAME}#{index}.txt"
|
||||
end
|
||||
|
||||
def commit_error_exception(error)
|
||||
if error.is_a?(Gitlab::Git::Index::IndexError) && invalid_path_error?(error.message)
|
||||
def commit_error_exception(err)
|
||||
if invalid_path_error?(err)
|
||||
InvalidPathError.new('Invalid Path') # To avoid returning the message with the path included
|
||||
elsif invalid_signature_error?(err)
|
||||
InvalidSignatureError.new(err.message)
|
||||
else
|
||||
CommitError.new(error.message)
|
||||
CommitError.new(err.message)
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_path_error?(message)
|
||||
message.downcase.start_with?('invalid path', 'path cannot include directory traversal')
|
||||
def invalid_path_error?(err)
|
||||
err.is_a?(Gitlab::Git::Index::IndexError) &&
|
||||
err.message.downcase.start_with?('invalid path', 'path cannot include directory traversal')
|
||||
end
|
||||
|
||||
def invalid_signature_error?(err)
|
||||
err.is_a?(ArgumentError) &&
|
||||
err.message.downcase.match?(/failed to parse signature/)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,16 @@ module Prometheus
|
|||
class ProxyVariableSubstitutionService < BaseService
|
||||
include Stepable
|
||||
|
||||
VARIABLE_INTERPOLATION_REGEX = /
|
||||
{{ # Variable needs to be wrapped in these chars.
|
||||
\s* # Allow whitespace before and after the variable name.
|
||||
(?<variable> # Named capture.
|
||||
\w+ # Match one or more word characters.
|
||||
)
|
||||
\s*
|
||||
}}
|
||||
/x.freeze
|
||||
|
||||
steps :validate_variables,
|
||||
:add_params_to_result,
|
||||
:substitute_params,
|
||||
|
@ -49,12 +59,9 @@ module Prometheus
|
|||
def substitute_liquid_variables(result)
|
||||
return success(result) unless query(result)
|
||||
|
||||
result[:params][:query] =
|
||||
TemplateEngines::LiquidService.new(query(result)).render(full_context)
|
||||
result[:params][:query] = gsub(query(result), full_context)
|
||||
|
||||
success(result)
|
||||
rescue TemplateEngines::LiquidService::RenderError => e
|
||||
error(e.message)
|
||||
end
|
||||
|
||||
def substitute_ruby_variables(result)
|
||||
|
@ -75,12 +82,24 @@ module Prometheus
|
|||
error(_('Malformed string'))
|
||||
end
|
||||
|
||||
def gsub(string, context)
|
||||
# Search for variables of the form `{{variable}}` in the string and replace
|
||||
# them with their value.
|
||||
string.gsub(VARIABLE_INTERPOLATION_REGEX) do |match|
|
||||
# Replace with the value of the variable, or if there is no such variable,
|
||||
# replace the invalid variable with itself. So,
|
||||
# `up{instance="{{invalid_variable}}"}` will remain
|
||||
# `up{instance="{{invalid_variable}}"}` after substitution.
|
||||
context.fetch($~[:variable], match)
|
||||
end
|
||||
end
|
||||
|
||||
def predefined_context
|
||||
@predefined_context ||= Gitlab::Prometheus::QueryVariables.call(@environment)
|
||||
end
|
||||
|
||||
def full_context
|
||||
@full_context ||= predefined_context.reverse_merge(variables_hash)
|
||||
@full_context ||= predefined_context.stringify_keys.reverse_merge(variables_hash)
|
||||
end
|
||||
|
||||
def variables
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module TemplateEngines
|
||||
class LiquidService < BaseService
|
||||
RenderError = Class.new(StandardError)
|
||||
|
||||
DEFAULT_RENDER_SCORE_LIMIT = 1_000
|
||||
|
||||
def initialize(string)
|
||||
@template = Liquid::Template.parse(string)
|
||||
end
|
||||
|
||||
def render(context, render_score_limit: DEFAULT_RENDER_SCORE_LIMIT)
|
||||
set_limits(render_score_limit)
|
||||
|
||||
@template.render!(context.stringify_keys)
|
||||
rescue Liquid::MemoryError => e
|
||||
handle_exception(e, string: @string, context: context)
|
||||
|
||||
raise RenderError, _('Memory limit exceeded while rendering template')
|
||||
rescue Liquid::Error => e
|
||||
handle_exception(e, string: @string, context: context)
|
||||
|
||||
raise RenderError, _('Error rendering query')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_limits(render_score_limit)
|
||||
@template.resource_limits.render_score_limit = render_score_limit
|
||||
|
||||
# We can also set assign_score_limit and render_length_limit if required.
|
||||
|
||||
# render_score_limit limits the number of nodes (string, variable, block, tags)
|
||||
# that are allowed in the template.
|
||||
# render_length_limit seems to limit the sum of the bytesize of all node blocks.
|
||||
# assign_score_limit seems to limit the sum of the bytesize of all capture blocks.
|
||||
end
|
||||
|
||||
def handle_exception(exception, extra = {})
|
||||
log_error(exception.message)
|
||||
Gitlab::ErrorTracking.track_exception(exception, {
|
||||
template_string: extra[:string],
|
||||
variables: extra[:context]
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -961,7 +961,7 @@
|
|||
:urgency: :low
|
||||
:resource_boundary: :unknown
|
||||
:weight: 2
|
||||
:idempotent:
|
||||
:idempotent: true
|
||||
- :name: create_evidence
|
||||
:feature_category: :release_evidence
|
||||
:has_external_dependencies:
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateCommitSignatureWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
class CreateCommitSignatureWorker
|
||||
include ApplicationWorker
|
||||
|
||||
feature_category :source_code_management
|
||||
weight 2
|
||||
|
||||
idempotent!
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def perform(commit_shas, project_id)
|
||||
# Older versions of Git::BranchPushService may push a single commit ID on
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add elipsis to container registry tag name
|
||||
merge_request: 31584
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: app:gitlab:check rake task now warns when projects are not in hashed storage
|
||||
merge_request: 31172
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Use gsub instead of the Liquid gem for variable substitution in the Prometheus
|
||||
proxy API
|
||||
merge_request: 31482
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix snippet migration when user has invalid info
|
||||
merge_request: 31488
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Use iso 8601 timestamp format in metrics dashboard annotations graphql resource
|
||||
to assure multi browser compatibility
|
||||
merge_request: 31474
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Rubocop cop to flag keyword arguments usage in Sidekiq workers
|
||||
merge_request: 31551
|
||||
author: Arun Kumar Mohan
|
||||
type: added
|
|
@ -7,11 +7,11 @@ For a full list of reference architectures, see
|
|||
> - **Supported users (approximate):** 1,000
|
||||
> - **High Availability:** False
|
||||
|
||||
| Users | Configuration([8](#footnotes)) | GCP type | AWS type([9](#footnotes)) |
|
||||
|-------|--------------------------------|---------------|---------------------------|
|
||||
| 100 | 2 vCPU, 7.2GB Memory | n1-standard-2 | c5.2xlarge |
|
||||
| 500 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge |
|
||||
| 1000 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge |
|
||||
| Users | Configuration([8](#footnotes)) | GCP | AWS([9](#footnotes)) | Azure([9](#footnotes)) |
|
||||
|-------|--------------------------------|---------------|----------------------|------------------------|
|
||||
| 100 | 2 vCPU, 7.2GB Memory | n1-standard-2 | m5.large | D2s v3 |
|
||||
| 500 | 4 vCPU, 15GB Memory | n1-standard-4 | m5.xlarge | D4s v3 |
|
||||
| 1000 | 8 vCPU, 30GB Memory | n1-standard-8 | m5.2xlarge | D8s v3 |
|
||||
|
||||
For situations where you need to serve up to 1,000 users, a single-node
|
||||
solution with [frequent backups](index.md#automated-backups-core-only) is appropriate
|
||||
|
|
|
@ -6076,7 +6076,7 @@ type MetricsDashboardAnnotation {
|
|||
"""
|
||||
Timestamp marking end of annotated time span
|
||||
"""
|
||||
endingAt: String
|
||||
endingAt: Time
|
||||
|
||||
"""
|
||||
ID of the annotation
|
||||
|
@ -6091,7 +6091,7 @@ type MetricsDashboardAnnotation {
|
|||
"""
|
||||
Timestamp marking start of annotated time span
|
||||
"""
|
||||
startingAt: String
|
||||
startingAt: Time
|
||||
}
|
||||
|
||||
"""
|
||||
|
|
|
@ -17041,7 +17041,7 @@
|
|||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"name": "Time",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
|
@ -17087,7 +17087,7 @@
|
|||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"name": "Time",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
|
|
|
@ -916,10 +916,10 @@ Autogenerated return type of MergeRequestSetWip
|
|||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `description` | String | Description of the annotation |
|
||||
| `endingAt` | String | Timestamp marking end of annotated time span |
|
||||
| `endingAt` | Time | Timestamp marking end of annotated time span |
|
||||
| `id` | ID! | ID of the annotation |
|
||||
| `panelId` | String | ID of a dashboard panel to which the annotation should be scoped |
|
||||
| `startingAt` | String | Timestamp marking start of annotated time span |
|
||||
| `startingAt` | Time | Timestamp marking start of annotated time span |
|
||||
|
||||
## Milestone
|
||||
|
||||
|
|
|
@ -135,11 +135,11 @@ third party ports for other languages like JavaScript, Python, Ruby, and so on.
|
|||
|
||||
#### `artifacts:reports:terraform`
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/207527) in GitLab 12.10.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207528) in GitLab 13.0.
|
||||
> - Requires [GitLab Runner](https://docs.gitlab.com/runner/) 11.5 and above.
|
||||
|
||||
The `terraform` report collects Terraform `tfplan.json` files. The collected Terraform
|
||||
plan reports will be uploaded to GitLab as artifacts and will be automatically shown
|
||||
The `terraform` report obtains a Terraform `tfplan.json` file. The collected Terraform
|
||||
plan report will be uploaded to GitLab as an artifact and will be automatically shown
|
||||
in merge requests.
|
||||
|
||||
#### `artifacts:reports:codequality` **(STARTER)**
|
||||
|
|
|
@ -118,6 +118,7 @@ Once synchronized, changing the field mapped to `id` and `externalId` will likel
|
|||
### Okta configuration steps
|
||||
|
||||
The SAML application that was created during [Single sign-on](index.md#okta-setup-notes) setup for [Okta](https://developer.okta.com/docs/guides/saml-application-setup/overview/) now needs to be set up for SCIM.
|
||||
Before proceeding, be sure to complete the [GitLab configuration](#gitlab-configuration) process.
|
||||
|
||||
1. Sign in to Okta.
|
||||
1. If you see an **Admin** button in the top right, click the button. This will
|
||||
|
|
|
@ -20,7 +20,7 @@ by default. To enable it for existing projects, or if you want to disable it:
|
|||
1. Find the Packages feature and enable or disable it.
|
||||
1. Click on **Save changes** for the changes to take effect.
|
||||
|
||||
You should then be able to see the **Packages** section on the left sidebar.
|
||||
You should then be able to see the **Packages & Registries** section on the left sidebar.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
|
Before Width: | Height: | Size: 48 KiB |
After Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 24 KiB |
After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 37 KiB |
|
@ -19,14 +19,14 @@ have its own space to store its Docker images.
|
|||
|
||||
You can read more about Docker Registry at <https://docs.docker.com/registry/introduction/>.
|
||||
|
||||
![Container Registry repositories](img/container_registry_repositories_v12_10.png)
|
||||
![Container Registry repositories](img/container_registry_repositories_v13_0.png)
|
||||
|
||||
## Enable the Container Registry for your project
|
||||
|
||||
CAUTION: **Warning:**
|
||||
The Container Registry follows the visibility settings of the project. If the project is public, so is the Container Registry.
|
||||
|
||||
If you cannot find the **Packages > Container Registry** entry under your
|
||||
If you cannot find the **Packages & Registries > Container Registry** entry under your
|
||||
project's sidebar, it is not enabled in your GitLab instance. Ask your
|
||||
administrator to enable GitLab Container Registry following the
|
||||
[administration documentation](../../../administration/packages/container_registry.md).
|
||||
|
@ -44,7 +44,7 @@ project:
|
|||
projects this might be enabled by default. For existing projects
|
||||
(prior GitLab 8.8), you will have to explicitly enable it.
|
||||
1. Press **Save changes** for the changes to take effect. You should now be able
|
||||
to see the **Packages > Container Registry** link in the sidebar.
|
||||
to see the **Packages & Registries > Container Registry** link in the sidebar.
|
||||
|
||||
## Control Container Registry from within GitLab
|
||||
|
||||
|
@ -53,9 +53,9 @@ for both projects and groups.
|
|||
|
||||
### Control Container Registry for your project
|
||||
|
||||
Navigate to your project's **{package}** **Packages > Container Registry**.
|
||||
Navigate to your project's **{package}** **Packages & Registries > Container Registry**.
|
||||
|
||||
![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v12_10.png)
|
||||
![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v13_0.png)
|
||||
|
||||
This view will:
|
||||
|
||||
|
@ -67,9 +67,9 @@ This view will:
|
|||
|
||||
### Control Container Registry for your group
|
||||
|
||||
Navigate to your groups's **{package}** **Packages > Container Registry**.
|
||||
Navigate to your groups's **{package}** **Packages & Registries > Container Registry**.
|
||||
|
||||
![Container Registry group repositories](img/container_registry_group_repositories_v12_10.png)
|
||||
![Container Registry group repositories](img/container_registry_group_repositories_v13_0.png)
|
||||
|
||||
This view will:
|
||||
|
||||
|
@ -81,7 +81,7 @@ This view will:
|
|||
|
||||
Clicking on the name of any image repository will navigate to the details.
|
||||
|
||||
![Container Registry project repository details](img/container_registry_repository_details_v12.10.png)
|
||||
![Container Registry project repository details](img/container_registry_repository_details_v13.0.png)
|
||||
|
||||
NOTE: **Note:**
|
||||
The following page has the same functionalities both in the **Group level container registry**
|
||||
|
@ -108,7 +108,7 @@ For more information on running Docker containers, visit the
|
|||
|
||||
## Authenticating to the GitLab Container Registry
|
||||
|
||||
If you visit the **Packages > Container Registry** link under your project's
|
||||
If you visit the **Packages & Registries > Container Registry** link under your project's
|
||||
menu, you can see the explicit instructions to login to the Container Registry
|
||||
using your GitLab credentials.
|
||||
|
||||
|
@ -389,7 +389,7 @@ the deleted images.
|
|||
|
||||
To delete images from within GitLab:
|
||||
|
||||
1. Navigate to your project's or group's **{package}** **Packages > Container Registry**.
|
||||
1. Navigate to your project's or group's **{package}** **Packages & Registries > Container Registry**.
|
||||
1. From the **Container Registry** page, you can select what you want to delete,
|
||||
by either:
|
||||
|
||||
|
@ -401,7 +401,7 @@ To delete images from within GitLab:
|
|||
|
||||
1. In the dialog box, click **Remove tag**.
|
||||
|
||||
![Container Registry tags](img/container_registry_tags_v12_10.png)
|
||||
![Container Registry tags](img/container_registry_repository_details_v13.0.png)
|
||||
|
||||
### Delete images using the API
|
||||
|
||||
|
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 29 KiB |
|
@ -12,7 +12,7 @@ receiving a request and returning the upstream image from a registry, acting
|
|||
as a pull-through cache.
|
||||
|
||||
The dependency proxy is available in the group level. To access it, navigate to
|
||||
a group's **Packages > Dependency Proxy**.
|
||||
a group's **Packages & Registries > Dependency Proxy**.
|
||||
|
||||
![Dependency Proxy group page](img/group_dependency_proxy.png)
|
||||
|
||||
|
@ -33,7 +33,7 @@ The following dependency proxies are supported.
|
|||
With the Docker dependency proxy, you can use GitLab as a source for a Docker image.
|
||||
To get a Docker image into the dependency proxy:
|
||||
|
||||
1. Find the proxy URL on your group's page under **Packages > Dependency Proxy**,
|
||||
1. Find the proxy URL on your group's page under **Packages & Registries > Dependency Proxy**,
|
||||
for example `gitlab.com/groupname/dependency_proxy/containers`.
|
||||
1. Trigger GitLab to pull the Docker image you want (e.g., `alpine:latest` or
|
||||
`linuxserver/nextcloud:latest`) and store it in the proxy storage by using
|
||||
|
|
Before Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 62 KiB |
After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 52 KiB |
|
@ -18,7 +18,7 @@ The Packages feature allows GitLab to act as a repository for the following:
|
|||
|
||||
## Enable the Package Registry for your project
|
||||
|
||||
If you cannot find the **{package}** **Packages > List** entry under your
|
||||
If you cannot find the **{package}** **Packages & Registries > Package Registry** entry under your
|
||||
project's sidebar, it is not enabled in your GitLab instance. Ask your
|
||||
administrator to enable GitLab Package Registry following the [administration
|
||||
documentation](../../administration/packages/index.md).
|
||||
|
@ -30,14 +30,14 @@ project:
|
|||
1. Expand the **Visibility, project features, permissions** section and enable the
|
||||
**Packages** feature on your project.
|
||||
1. Press **Save changes** for the changes to take effect. You should now be able to
|
||||
see the **Packages > List** link in the sidebar.
|
||||
see the **Packages & Registries > Package Registry** link in the sidebar.
|
||||
|
||||
### View Packages for your project
|
||||
|
||||
Navigating to your project's **{package}** **Packages > List** will show a list
|
||||
Navigating to your project's **{package}** **Packages & Registries > Package Registry** will show a list
|
||||
of all packages that have been added to your project.
|
||||
|
||||
![Project Packages list](img/project_packages_list_v12_10.png)
|
||||
![Project Packages list](img/project_packages_list_v13_0.png)
|
||||
|
||||
On this page, you can:
|
||||
|
||||
|
@ -51,9 +51,9 @@ On this page, you can:
|
|||
### View Packages for your group
|
||||
|
||||
You can view all packages belonging to a group by navigating to **{package}**
|
||||
**Packages > List** from the group sidebar.
|
||||
**Packages & Registries > Package Registry** from the group sidebar.
|
||||
|
||||
![Group Packages list](img/group_packages_list_v12_10.png)
|
||||
![Group Packages list](img/group_packages_list_v13_0.png)
|
||||
|
||||
On this page, you can:
|
||||
|
||||
|
@ -68,7 +68,7 @@ On this page, you can:
|
|||
Additional package information can be viewed by browsing to the package details
|
||||
page from the either the project or group list.
|
||||
|
||||
![Package detail](img/package_detail_v12_10.png)
|
||||
![Package detail](img/package_detail_v13_0.png)
|
||||
|
||||
On this page you can:
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ to disable it:
|
|||
1. Find the Packages feature and enable or disable it.
|
||||
1. Click on **Save changes** for the changes to take effect.
|
||||
|
||||
You should then be able to see the **Packages** section on the left sidebar.
|
||||
You should then be able to see the **Packages & Registries** section on the left sidebar.
|
||||
Next, you must configure your project to authorize with the GitLab Maven
|
||||
repository.
|
||||
|
||||
|
@ -595,7 +595,7 @@ Run the publish task:
|
|||
gradle publish
|
||||
```
|
||||
|
||||
You can then navigate to your project's **Packages** page and see the uploaded
|
||||
You can then navigate to your project's **Packages & Registries** page and see the uploaded
|
||||
artifacts or even delete them.
|
||||
|
||||
## Installing a package
|
||||
|
|
|
@ -23,7 +23,7 @@ by default. To enable it for existing projects, or if you want to disable it:
|
|||
1. Find the Packages feature and enable or disable it.
|
||||
1. Click on **Save changes** for the changes to take effect.
|
||||
|
||||
You should then be able to see the **Packages** section on the left sidebar.
|
||||
You should then be able to see the **Packages & Registries** section on the left sidebar.
|
||||
|
||||
Before proceeding to authenticating with the GitLab NPM Registry, you should
|
||||
get familiar with the package naming convention.
|
||||
|
@ -195,7 +195,7 @@ you can upload an NPM package to your project:
|
|||
npm publish
|
||||
```
|
||||
|
||||
You can then navigate to your project's **Packages** page and see the uploaded
|
||||
You can then navigate to your project's **Packages & Registries** page and see the uploaded
|
||||
packages or even delete them.
|
||||
|
||||
If you attempt to publish a package with a name that already exists within
|
||||
|
|
|
@ -61,7 +61,7 @@ by default. To enable it for existing projects, or if you want to disable it:
|
|||
1. Find the Packages feature and enable or disable it.
|
||||
1. Click on **Save changes** for the changes to take effect.
|
||||
|
||||
You should then be able to see the **Packages** section on the left sidebar.
|
||||
You should then be able to see the **Packages & Registries** section on the left sidebar.
|
||||
|
||||
## Adding the GitLab NuGet Repository as a source to NuGet
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ by default. To enable it for existing projects, or if you want to disable it:
|
|||
1. Find the Packages feature and enable or disable it.
|
||||
1. Click on **Save changes** for the changes to take effect.
|
||||
|
||||
You should then be able to see the **Packages** section on the left sidebar.
|
||||
You should then be able to see the **Packages & Registries** section on the left sidebar.
|
||||
|
||||
## Getting started
|
||||
|
||||
|
@ -197,7 +197,7 @@ Uploading mypypipackage-0.0.1.tar.gz
|
|||
```
|
||||
|
||||
This indicates that the package was uploaded successfully. You can then navigate
|
||||
to your project's **Packages** page and see the uploaded packages.
|
||||
to your project's **Packages & Registries** page and see the uploaded packages.
|
||||
|
||||
If you did not follow the guide above, the you'll need to ensure your package
|
||||
has been properly built and you [created a PyPi package with setuptools](https://packaging.python.org/tutorials/packaging-projects/).
|
||||
|
|
|
@ -194,7 +194,7 @@ Variables for Prometheus queries must be lowercase.
|
|||
|
||||
There are 2 methods to specify a variable in a query or dashboard:
|
||||
|
||||
1. Variables can be specified using the [Liquid template format](https://shopify.dev/docs/liquid/reference/basics), for example `{{ci_environment_slug}}` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.6).
|
||||
1. Variables can be specified using double curly braces, such as `{{ci_environment_slug}}` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7).
|
||||
1. You can also enclose it in quotation marks with curly braces with a leading percent, for example `"%{ci_environment_slug}"`. This method is deprecated though and support will be [removed in the next major release](https://gitlab.com/gitlab-org/gitlab/issues/37990).
|
||||
|
||||
#### Query Variables from URL
|
||||
|
|
|
@ -16,6 +16,7 @@ module Gitlab
|
|||
|
||||
retry_index = 0
|
||||
@invalid_path_error = false
|
||||
@invalid_signature_error = false
|
||||
|
||||
begin
|
||||
create_repository_and_files(snippet)
|
||||
|
@ -23,10 +24,11 @@ module Gitlab
|
|||
logger.info(message: 'Snippet Migration: repository created and migrated', snippet: snippet.id)
|
||||
rescue => e
|
||||
set_file_path_error(e)
|
||||
set_signature_error(e)
|
||||
|
||||
retry_index += 1
|
||||
|
||||
retry if retry_index < MAX_RETRIES
|
||||
retry if retry_index < max_retries
|
||||
|
||||
logger.error(message: "Snippet Migration: error migrating snippet. Reason: #{e.message}", snippet: snippet.id)
|
||||
|
||||
|
@ -101,6 +103,7 @@ module Gitlab
|
|||
# In this scenario the migration bot user will be the one that will commit the files.
|
||||
def commit_author(snippet)
|
||||
return migration_bot_user if snippet_content_size_over_limit?(snippet)
|
||||
return migration_bot_user if @invalid_signature_error
|
||||
|
||||
if Gitlab::UserAccessSnippet.new(snippet.author, snippet: snippet).can_do_action?(:update_snippet)
|
||||
snippet.author
|
||||
|
@ -119,7 +122,23 @@ module Gitlab
|
|||
# the migration can succeed, to achieve that, we'll identify in migration retries
|
||||
# that the path is invalid
|
||||
def set_file_path_error(error)
|
||||
@invalid_path_error = error.is_a?(SnippetRepository::InvalidPathError)
|
||||
@invalid_path_error ||= error.is_a?(SnippetRepository::InvalidPathError)
|
||||
end
|
||||
|
||||
# We sometimes receive invalid signature from Gitaly if the commit author
|
||||
# name or email is invalid to create the commit signature.
|
||||
# In this situation, we set the error and use the migration_bot since
|
||||
# the information used to build it is valid
|
||||
def set_signature_error(error)
|
||||
@invalid_signature_error ||= error.is_a?(SnippetRepository::InvalidSignatureError)
|
||||
end
|
||||
|
||||
# In the case where the snippet file_name is invalid and also the
|
||||
# snippet author has invalid commit info, we need to increase the
|
||||
# number of retries by 1, because we will receive two errors
|
||||
# from Gitaly and, in the third one, we will commit successfully.
|
||||
def max_retries
|
||||
MAX_RETRIES + (@invalid_signature_error && @invalid_path_error ? 1 : 0)
|
||||
end
|
||||
|
||||
def snippet_content_size_over_limit?(snippet)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SystemCheck
|
||||
module App
|
||||
class HashedStorageAllProjectsCheck < SystemCheck::BaseCheck
|
||||
set_name 'All projects are in hashed storage?'
|
||||
|
||||
def check?
|
||||
!Project.with_unmigrated_storage.exists?
|
||||
end
|
||||
|
||||
def show_error
|
||||
try_fixing_it(
|
||||
"Please migrate all projects to hashed storage#{' on the primary' if Gitlab.ee? && Gitlab::Geo.secondary?}",
|
||||
"as legacy storage is deprecated in 13.0 and support will be removed in 13.4."
|
||||
)
|
||||
|
||||
for_more_information('doc/administration/repository_storage_types.md')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module SystemCheck
|
||||
module App
|
||||
class HashedStorageEnabledCheck < SystemCheck::BaseCheck
|
||||
set_name 'GitLab configured to store new projects in hashed storage?'
|
||||
|
||||
def check?
|
||||
Gitlab::CurrentSettings.current_application_settings.hashed_storage_enabled
|
||||
end
|
||||
|
||||
def show_error
|
||||
try_fixing_it(
|
||||
"Please enable the setting",
|
||||
"`Use hashed storage paths for newly created and renamed projects`",
|
||||
"in GitLab's Admin panel to avoid security issues and ensure data integrity."
|
||||
)
|
||||
|
||||
for_more_information('doc/administration/repository_storage_types.md')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -31,7 +31,9 @@ module SystemCheck
|
|||
SystemCheck::App::GitVersionCheck,
|
||||
SystemCheck::App::GitUserDefaultSSHConfigCheck,
|
||||
SystemCheck::App::ActiveUsersCheck,
|
||||
SystemCheck::App::AuthorizedKeysPermissionCheck
|
||||
SystemCheck::App::AuthorizedKeysPermissionCheck,
|
||||
SystemCheck::App::HashedStorageEnabledCheck,
|
||||
SystemCheck::App::HashedStorageAllProjectsCheck
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1069,12 +1069,6 @@ msgstr ""
|
|||
msgid "AccessTokens|reset it"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessibilityReport|Accessibility report artifact not found"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessibilityReport|Failed to retrieve accessibility report"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessibilityReport|Learn More"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8519,9 +8513,6 @@ msgstr ""
|
|||
msgid "Error rendering markdown preview"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error rendering query"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error saving label update."
|
||||
msgstr ""
|
||||
|
||||
|
@ -12998,9 +12989,6 @@ msgstr ""
|
|||
msgid "Memory Usage"
|
||||
msgstr ""
|
||||
|
||||
msgid "Memory limit exceeded while rendering template"
|
||||
msgstr ""
|
||||
|
||||
msgid "Merge"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
module RuboCop
|
||||
module Cop
|
||||
# Cop that blacklists keyword arguments usage in Sidekiq workers
|
||||
class AvoidKeywordArgumentsInSidekiqWorkers < RuboCop::Cop::Cop
|
||||
MSG = "Do not use keyword arguments in Sidekiq workers. " \
|
||||
"For details, check https://github.com/mperham/sidekiq/issues/2372".freeze
|
||||
OBSERVED_METHOD = :perform
|
||||
|
||||
def on_def(node)
|
||||
return if node.method_name != OBSERVED_METHOD
|
||||
|
||||
node.arguments.each do |argument|
|
||||
if argument.type == :kwarg || argument.type == :kwoptarg
|
||||
add_offense(node, location: :expression)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -560,3 +560,214 @@ export const mockNamespacedData = {
|
|||
export const mockLogsPath = '/mockLogsPath';
|
||||
|
||||
export const mockLogsHref = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`;
|
||||
|
||||
const templatingVariableTypes = {
|
||||
text: {
|
||||
simple: 'Simple text',
|
||||
advanced: {
|
||||
label: 'Variable 4',
|
||||
type: 'text',
|
||||
options: {
|
||||
default_value: 'default',
|
||||
},
|
||||
},
|
||||
},
|
||||
custom: {
|
||||
simple: ['value1', 'value2', 'value3'],
|
||||
advanced: {
|
||||
normal: {
|
||||
label: 'Advanced Var',
|
||||
type: 'custom',
|
||||
options: {
|
||||
values: [
|
||||
{ value: 'value1', text: 'Var 1 Option 1' },
|
||||
{
|
||||
value: 'value2',
|
||||
text: 'Var 1 Option 2',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
withoutOpts: {
|
||||
type: 'custom',
|
||||
options: {},
|
||||
},
|
||||
withoutLabel: {
|
||||
type: 'custom',
|
||||
options: {
|
||||
values: [
|
||||
{ value: 'value1', text: 'Var 1 Option 1' },
|
||||
{
|
||||
value: 'value2',
|
||||
text: 'Var 1 Option 2',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
withoutType: {
|
||||
label: 'Variable 2',
|
||||
options: {
|
||||
values: [
|
||||
{ value: 'value1', text: 'Var 1 Option 1' },
|
||||
{
|
||||
value: 'value2',
|
||||
text: 'Var 1 Option 2',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const generateMockTemplatingData = data => {
|
||||
const vars = data
|
||||
? {
|
||||
variables: {
|
||||
...data,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
dashboard: {
|
||||
templating: vars,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const responseForSimpleTextVariable = {
|
||||
simpleText: {
|
||||
label: 'simpleText',
|
||||
type: 'text',
|
||||
value: 'Simple text',
|
||||
},
|
||||
};
|
||||
|
||||
const responseForAdvTextVariable = {
|
||||
advText: {
|
||||
label: 'Variable 4',
|
||||
type: 'text',
|
||||
value: 'default',
|
||||
},
|
||||
};
|
||||
|
||||
const responseForSimpleCustomVariable = {
|
||||
simpleCustom: {
|
||||
label: 'simpleCustom',
|
||||
options: [
|
||||
{
|
||||
default: false,
|
||||
text: 'value1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
text: 'value2',
|
||||
value: 'value2',
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
text: 'value3',
|
||||
value: 'value3',
|
||||
},
|
||||
],
|
||||
type: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
const responseForAdvancedCustomVariableWithoutOptions = {
|
||||
advCustomWithoutOpts: {
|
||||
label: 'advCustomWithoutOpts',
|
||||
options: [],
|
||||
type: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
const responseForAdvancedCustomVariableWithoutLabel = {
|
||||
advCustomWithoutLabel: {
|
||||
label: 'advCustomWithoutLabel',
|
||||
options: [
|
||||
{
|
||||
default: false,
|
||||
text: 'Var 1 Option 1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
text: 'Var 1 Option 2',
|
||||
value: 'value2',
|
||||
},
|
||||
],
|
||||
type: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
const responseForAdvancedCustomVariable = {
|
||||
...responseForSimpleCustomVariable,
|
||||
advCustomNormal: {
|
||||
label: 'Advanced Var',
|
||||
options: [
|
||||
{
|
||||
default: false,
|
||||
text: 'Var 1 Option 1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
text: 'Var 1 Option 2',
|
||||
value: 'value2',
|
||||
},
|
||||
],
|
||||
type: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
const responsesForAllVariableTypes = {
|
||||
...responseForSimpleTextVariable,
|
||||
...responseForAdvTextVariable,
|
||||
...responseForSimpleCustomVariable,
|
||||
...responseForAdvancedCustomVariable,
|
||||
};
|
||||
|
||||
export const mockTemplatingData = {
|
||||
emptyTemplatingProp: generateMockTemplatingData(),
|
||||
emptyVariablesProp: generateMockTemplatingData({}),
|
||||
simpleText: generateMockTemplatingData({ simpleText: templatingVariableTypes.text.simple }),
|
||||
advText: generateMockTemplatingData({ advText: templatingVariableTypes.text.advanced }),
|
||||
simpleCustom: generateMockTemplatingData({ simpleCustom: templatingVariableTypes.custom.simple }),
|
||||
advCustomWithoutOpts: generateMockTemplatingData({
|
||||
advCustomWithoutOpts: templatingVariableTypes.custom.advanced.withoutOpts,
|
||||
}),
|
||||
advCustomWithoutType: generateMockTemplatingData({
|
||||
advCustomWithoutType: templatingVariableTypes.custom.advanced.withoutType,
|
||||
}),
|
||||
advCustomWithoutLabel: generateMockTemplatingData({
|
||||
advCustomWithoutLabel: templatingVariableTypes.custom.advanced.withoutLabel,
|
||||
}),
|
||||
simpleAndAdv: generateMockTemplatingData({
|
||||
simpleCustom: templatingVariableTypes.custom.simple,
|
||||
advCustomNormal: templatingVariableTypes.custom.advanced.normal,
|
||||
}),
|
||||
allVariableTypes: generateMockTemplatingData({
|
||||
simpleText: templatingVariableTypes.text.simple,
|
||||
advText: templatingVariableTypes.text.advanced,
|
||||
simpleCustom: templatingVariableTypes.custom.simple,
|
||||
advCustomNormal: templatingVariableTypes.custom.advanced.normal,
|
||||
}),
|
||||
};
|
||||
|
||||
export const mockTemplatingDataResponses = {
|
||||
emptyTemplatingProp: {},
|
||||
emptyVariablesProp: {},
|
||||
simpleText: responseForSimpleTextVariable,
|
||||
advText: responseForAdvTextVariable,
|
||||
simpleCustom: responseForSimpleCustomVariable,
|
||||
advCustomWithoutOpts: responseForAdvancedCustomVariableWithoutOptions,
|
||||
advCustomWithoutType: {},
|
||||
advCustomWithoutLabel: responseForAdvancedCustomVariableWithoutLabel,
|
||||
simpleAndAdv: responseForAdvancedCustomVariable,
|
||||
allVariableTypes: responsesForAllVariableTypes,
|
||||
};
|
||||
|
|
|
@ -1,149 +1,21 @@
|
|||
import { parseTemplatingVariables } from '~/monitoring/stores/variable_mapping';
|
||||
import { mockTemplatingData, mockTemplatingDataResponses } from '../mock_data';
|
||||
|
||||
describe('parseTemplatingVariables', () => {
|
||||
const generateMockTemplatingData = data => {
|
||||
const vars = data
|
||||
? {
|
||||
variables: {
|
||||
...data,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
dashboard: {
|
||||
templating: vars,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const simpleVar = ['value1', 'value2', 'value3'];
|
||||
const advVar = {
|
||||
label: 'Advanced Var',
|
||||
type: 'custom',
|
||||
options: {
|
||||
values: [
|
||||
{ value: 'value1', text: 'Var 1 Option 1' },
|
||||
{
|
||||
value: 'value2',
|
||||
text: 'Var 1 Option 2',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const advVarWithoutOptions = {
|
||||
type: 'custom',
|
||||
options: {},
|
||||
};
|
||||
const advVarWithoutLabel = {
|
||||
type: 'custom',
|
||||
options: {
|
||||
values: [
|
||||
{ value: 'value1', text: 'Var 1 Option 1' },
|
||||
{
|
||||
value: 'value2',
|
||||
text: 'Var 1 Option 2',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const advVarWithoutType = {
|
||||
label: 'Variable 2',
|
||||
options: {
|
||||
values: [
|
||||
{ value: 'value1', text: 'Var 1 Option 1' },
|
||||
{
|
||||
value: 'value2',
|
||||
text: 'Var 1 Option 2',
|
||||
default: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const responseForSimpleCustomVariable = {
|
||||
simpleVar: {
|
||||
label: 'simpleVar',
|
||||
options: [
|
||||
{
|
||||
default: false,
|
||||
text: 'value1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
text: 'value2',
|
||||
value: 'value2',
|
||||
},
|
||||
{
|
||||
default: false,
|
||||
text: 'value3',
|
||||
value: 'value3',
|
||||
},
|
||||
],
|
||||
type: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
const responseForAdvancedCustomVariableWithoutOptions = {
|
||||
advVarWithoutOptions: {
|
||||
label: 'advVarWithoutOptions',
|
||||
options: [],
|
||||
type: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
const responseForAdvancedCustomVariableWithoutLabel = {
|
||||
advVarWithoutLabel: {
|
||||
label: 'advVarWithoutLabel',
|
||||
options: [
|
||||
{
|
||||
default: false,
|
||||
text: 'Var 1 Option 1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
text: 'Var 1 Option 2',
|
||||
value: 'value2',
|
||||
},
|
||||
],
|
||||
type: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
const responseForAdvancedCustomVariable = {
|
||||
...responseForSimpleCustomVariable,
|
||||
advVar: {
|
||||
label: 'Advanced Var',
|
||||
options: [
|
||||
{
|
||||
default: false,
|
||||
text: 'Var 1 Option 1',
|
||||
value: 'value1',
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
text: 'Var 1 Option 2',
|
||||
value: 'value2',
|
||||
},
|
||||
],
|
||||
type: 'custom',
|
||||
},
|
||||
};
|
||||
|
||||
it.each`
|
||||
case | input | expected
|
||||
${'Returns empty object for no dashboard input'} | ${{}} | ${{}}
|
||||
${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}}
|
||||
${'Returns empty object for empty templating prop'} | ${generateMockTemplatingData()} | ${{}}
|
||||
${'Returns empty object for empty variables prop'} | ${generateMockTemplatingData({})} | ${{}}
|
||||
${'Returns parsed object for simple variable'} | ${generateMockTemplatingData({ simpleVar })} | ${responseForSimpleCustomVariable}
|
||||
${'Returns parsed object for advanced variable without options'} | ${generateMockTemplatingData({ advVarWithoutOptions })} | ${responseForAdvancedCustomVariableWithoutOptions}
|
||||
${'Returns parsed object for advanced variable without type'} | ${generateMockTemplatingData({ advVarWithoutType })} | ${{}}
|
||||
${'Returns parsed object for advanced variable without label'} | ${generateMockTemplatingData({ advVarWithoutLabel })} | ${responseForAdvancedCustomVariableWithoutLabel}
|
||||
${'Returns parsed object for simple and advanced variables'} | ${generateMockTemplatingData({ simpleVar, advVar })} | ${responseForAdvancedCustomVariable}
|
||||
case | input | expected
|
||||
${'Returns empty object for no dashboard input'} | ${{}} | ${{}}
|
||||
${'Returns empty object for empty dashboard input'} | ${{ dashboard: {} }} | ${{}}
|
||||
${'Returns empty object for empty templating prop'} | ${mockTemplatingData.emptyTemplatingProp} | ${{}}
|
||||
${'Returns empty object for empty variables prop'} | ${mockTemplatingData.emptyVariablesProp} | ${{}}
|
||||
${'Returns parsed object for simple text variable'} | ${mockTemplatingData.simpleText} | ${mockTemplatingDataResponses.simpleText}
|
||||
${'Returns parsed object for advanced text variable'} | ${mockTemplatingData.advText} | ${mockTemplatingDataResponses.advText}
|
||||
${'Returns parsed object for simple custom variable'} | ${mockTemplatingData.simpleCustom} | ${mockTemplatingDataResponses.simpleCustom}
|
||||
${'Returns parsed object for advanced custom variable without options'} | ${mockTemplatingData.advCustomWithoutOpts} | ${mockTemplatingDataResponses.advCustomWithoutOpts}
|
||||
${'Returns parsed object for advanced custom variable without type'} | ${mockTemplatingData.advCustomWithoutType} | ${{}}
|
||||
${'Returns parsed object for advanced custom variable without label'} | ${mockTemplatingData.advCustomWithoutLabel} | ${mockTemplatingDataResponses.advCustomWithoutLabel}
|
||||
${'Returns parsed object for simple and advanced custom variables'} | ${mockTemplatingData.simpleAndAdv} | ${mockTemplatingDataResponses.simpleAndAdv}
|
||||
${'Returns parsed object for all variable types'} | ${mockTemplatingData.allVariableTypes} | ${mockTemplatingDataResponses.allVariableTypes}
|
||||
`('$case', ({ input, expected }) => {
|
||||
expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected);
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('Details Page', () => {
|
|||
const findAllCheckboxes = () => wrapper.findAll('.js-row-checkbox');
|
||||
const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked'));
|
||||
const findFirsTagColumn = () => wrapper.find('.js-tag-column');
|
||||
const findFirstTagNameText = () => wrapper.find('[data-testid="rowNameText"]');
|
||||
const findAlert = () => wrapper.find(GlAlert);
|
||||
|
||||
const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' }));
|
||||
|
@ -248,15 +249,24 @@ describe('Details Page', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('tag cell', () => {
|
||||
describe('name cell', () => {
|
||||
it('tag column has a tooltip with the tag name', () => {
|
||||
mountComponent();
|
||||
expect(findFirstTagNameText().attributes('title')).toBe(tagsListResponse.data[0].name);
|
||||
});
|
||||
|
||||
describe('on desktop viewport', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it('has class w-25', () => {
|
||||
it('table header has class w-25', () => {
|
||||
expect(findFirsTagColumn().classes()).toContain('w-25');
|
||||
});
|
||||
|
||||
it('tag column has the mw-m class', () => {
|
||||
expect(findFirstRowItem('rowName').classes()).toContain('mw-m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on mobile viewport', () => {
|
||||
|
@ -268,9 +278,13 @@ describe('Details Page', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not has class w-25', () => {
|
||||
it('table header does not have class w-25', () => {
|
||||
expect(findFirsTagColumn().classes()).not.toContain('w-25');
|
||||
});
|
||||
|
||||
it('tag column has the gl-justify-content-end class', () => {
|
||||
expect(findFirstRowItem('rowName').classes()).toContain('gl-justify-content-end');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import Vuex from 'vuex';
|
|||
import GroupedAccessibilityReportsApp from '~/reports/accessibility_report/grouped_accessibility_reports_app.vue';
|
||||
import AccessibilityIssueBody from '~/reports/accessibility_report/components/accessibility_issue_body.vue';
|
||||
import store from '~/reports/accessibility_report/store';
|
||||
import { comparedReportResult } from './mock_data';
|
||||
import { mockReport } from './mock_data';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
@ -18,8 +18,7 @@ describe('Grouped accessibility reports app', () => {
|
|||
store: mockStore,
|
||||
localVue,
|
||||
propsData: {
|
||||
baseEndpoint: 'base_endpoint.json',
|
||||
headEndpoint: 'head_endpoint.json',
|
||||
endpoint: 'endpoint.json',
|
||||
},
|
||||
methods: {
|
||||
fetchReport: () => {},
|
||||
|
@ -66,8 +65,7 @@ describe('Grouped accessibility reports app', () => {
|
|||
beforeEach(() => {
|
||||
mockStore.state.report = {
|
||||
summary: {
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
errored: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -83,8 +81,7 @@ describe('Grouped accessibility reports app', () => {
|
|||
beforeEach(() => {
|
||||
mockStore.state.report = {
|
||||
summary: {
|
||||
errors: 0,
|
||||
warnings: 1,
|
||||
errored: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -100,8 +97,7 @@ describe('Grouped accessibility reports app', () => {
|
|||
beforeEach(() => {
|
||||
mockStore.state.report = {
|
||||
summary: {
|
||||
errors: 1,
|
||||
warnings: 1,
|
||||
errored: 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -115,18 +111,15 @@ describe('Grouped accessibility reports app', () => {
|
|||
|
||||
describe('with issues to show', () => {
|
||||
beforeEach(() => {
|
||||
mockStore.state.report = comparedReportResult;
|
||||
mockStore.state.report = mockReport;
|
||||
});
|
||||
|
||||
it('renders custom accessibility issue body', () => {
|
||||
const issueBody = wrapper.find(AccessibilityIssueBody);
|
||||
|
||||
expect(issueBody.props('issue').name).toEqual(comparedReportResult.new_errors[0].name);
|
||||
expect(issueBody.props('issue').code).toEqual(comparedReportResult.new_errors[0].code);
|
||||
expect(issueBody.props('issue').message).toEqual(
|
||||
comparedReportResult.new_errors[0].message,
|
||||
);
|
||||
expect(issueBody.props('isNew')).toEqual(true);
|
||||
expect(issueBody.props('issue').code).toBe(mockReport.new_errors[0].code);
|
||||
expect(issueBody.props('issue').message).toBe(mockReport.new_errors[0].message);
|
||||
expect(issueBody.props('isNew')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,86 +1,55 @@
|
|||
export const baseReport = {
|
||||
results: {
|
||||
'http://about.gitlab.com/users/sign_in': [
|
||||
{
|
||||
code: 'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail',
|
||||
type: 'error',
|
||||
typeCode: 1,
|
||||
message:
|
||||
'This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 2.82:1. Recommendation: change background to #d1470c.',
|
||||
context:
|
||||
'<a class="btn btn-nav-cta btn-nav-link-cta" href="/free-trial">\nGet free trial\n</a>',
|
||||
selector: '#main-nav > div:nth-child(2) > ul > div:nth-child(8) > a',
|
||||
runner: 'htmlcs',
|
||||
runnerExtras: {},
|
||||
},
|
||||
],
|
||||
'https://about.gitlab.com': [
|
||||
{
|
||||
code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
|
||||
type: 'error',
|
||||
typeCode: 1,
|
||||
message:
|
||||
'Anchor element found with a valid href attribute, but no link content has been supplied.',
|
||||
context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
|
||||
selector: '#main-nav > div:nth-child(1) > a',
|
||||
runner: 'htmlcs',
|
||||
runnerExtras: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const parsedBaseReport = [
|
||||
'{"code":"WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail","type":"error","typeCode":1,"message":"This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 2.82:1. Recommendation: change background to #d1470c.","context":"<a class=\\"btn btn-nav-cta btn-nav-link-cta\\" href=\\"/free-trial\\">\\nGet free trial\\n</a>","selector":"#main-nav > div:nth-child(2) > ul > div:nth-child(8) > a","runner":"htmlcs","runnerExtras":{}}',
|
||||
'{"code":"WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent","type":"error","typeCode":1,"message":"Anchor element found with a valid href attribute, but no link content has been supplied.","context":"<a href=\\"/\\" class=\\"navbar-brand animated\\"><svg height=\\"36\\" viewBox=\\"0 0 1...</a>","selector":"#main-nav > div:nth-child(1) > a","runner":"htmlcs","runnerExtras":{}}',
|
||||
];
|
||||
|
||||
export const headReport = {
|
||||
results: {
|
||||
'http://about.gitlab.com/users/sign_in': [
|
||||
{
|
||||
code: 'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail',
|
||||
type: 'error',
|
||||
typeCode: 1,
|
||||
message:
|
||||
'This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 3.84:1. Recommendation: change text colour to #767676.',
|
||||
context: '<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>',
|
||||
selector: '#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a',
|
||||
runner: 'htmlcs',
|
||||
runnerExtras: {},
|
||||
},
|
||||
],
|
||||
'https://about.gitlab.com': [
|
||||
{
|
||||
code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
|
||||
type: 'error',
|
||||
typeCode: 1,
|
||||
message:
|
||||
'Anchor element found with a valid href attribute, but no link content has been supplied.',
|
||||
context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
|
||||
selector: '#main-nav > div:nth-child(1) > a',
|
||||
runner: 'htmlcs',
|
||||
runnerExtras: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const comparedReportResult = {
|
||||
export const mockReport = {
|
||||
status: 'failed',
|
||||
summary: {
|
||||
total: 2,
|
||||
notes: 0,
|
||||
errors: 2,
|
||||
warnings: 0,
|
||||
resolved: 0,
|
||||
errored: 2,
|
||||
},
|
||||
new_errors: [headReport.results['http://about.gitlab.com/users/sign_in'][0]],
|
||||
new_errors: [
|
||||
{
|
||||
code: 'WCAG2AA.Principle1.Guideline1_4.1_4_3.G18.Fail',
|
||||
type: 'error',
|
||||
typeCode: 1,
|
||||
message:
|
||||
'This element has insufficient contrast at this conformance level. Expected a contrast ratio of at least 4.5:1, but text in this element has a contrast ratio of 3.84:1. Recommendation: change text colour to #767676.',
|
||||
context: '<a href="/stages-devops-lifecycle/" class="main-nav-link">Product</a>',
|
||||
selector: '#main-nav > div:nth-child(2) > ul > li:nth-child(1) > a',
|
||||
runner: 'htmlcs',
|
||||
runnerExtras: {},
|
||||
},
|
||||
],
|
||||
new_notes: [],
|
||||
new_warnings: [],
|
||||
resolved_errors: [baseReport.results['http://about.gitlab.com/users/sign_in'][0]],
|
||||
resolved_errors: [
|
||||
{
|
||||
code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
|
||||
type: 'error',
|
||||
typeCode: 1,
|
||||
message:
|
||||
'Anchor element found with a valid href attribute, but no link content has been supplied.',
|
||||
context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
|
||||
selector: '#main-nav > div:nth-child(1) > a',
|
||||
runner: 'htmlcs',
|
||||
runnerExtras: {},
|
||||
},
|
||||
],
|
||||
resolved_notes: [],
|
||||
resolved_warnings: [],
|
||||
existing_errors: [headReport.results['https://about.gitlab.com'][0]],
|
||||
existing_errors: [
|
||||
{
|
||||
code: 'WCAG2AA.Principle4.Guideline4_1.4_1_2.H91.A.NoContent',
|
||||
type: 'error',
|
||||
typeCode: 1,
|
||||
message:
|
||||
'Anchor element found with a valid href attribute, but no link content has been supplied.',
|
||||
context: '<a href="/" class="navbar-brand animated"><svg height="36" viewBox="0 0 1...</a>',
|
||||
selector: '#main-nav > div:nth-child(1) > a',
|
||||
runner: 'htmlcs',
|
||||
runnerExtras: {},
|
||||
},
|
||||
],
|
||||
existing_notes: [],
|
||||
existing_warnings: [],
|
||||
};
|
||||
|
||||
export default () => {};
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as types from '~/reports/accessibility_report/store/mutation_types';
|
|||
import createStore from '~/reports/accessibility_report/store';
|
||||
import { TEST_HOST } from 'spec/test_constants';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
import { baseReport, headReport, comparedReportResult } from '../mock_data';
|
||||
import { mockReport } from '../mock_data';
|
||||
|
||||
describe('Accessibility Reports actions', () => {
|
||||
let localState;
|
||||
|
@ -18,14 +18,13 @@ describe('Accessibility Reports actions', () => {
|
|||
|
||||
describe('setEndpoints', () => {
|
||||
it('should commit SET_ENDPOINTS mutation', done => {
|
||||
const baseEndpoint = 'base_endpoint.json';
|
||||
const headEndpoint = 'head_endpoint.json';
|
||||
const endpoint = 'endpoint.json';
|
||||
|
||||
testAction(
|
||||
actions.setEndpoints,
|
||||
{ baseEndpoint, headEndpoint },
|
||||
actions.setEndpoint,
|
||||
endpoint,
|
||||
localState,
|
||||
[{ type: types.SET_ENDPOINTS, payload: { baseEndpoint, headEndpoint } }],
|
||||
[{ type: types.SET_ENDPOINT, payload: endpoint }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
|
@ -36,37 +35,14 @@ describe('Accessibility Reports actions', () => {
|
|||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
localState.baseEndpoint = `${TEST_HOST}/endpoint.json`;
|
||||
localState.headEndpoint = `${TEST_HOST}/endpoint.json`;
|
||||
localState.endpoint = `${TEST_HOST}/endpoint.json`;
|
||||
mock = new MockAdapter(axios);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
describe('when no endpoints are given', () => {
|
||||
beforeEach(() => {
|
||||
localState.baseEndpoint = null;
|
||||
localState.headEndpoint = null;
|
||||
});
|
||||
|
||||
it('should commit REQUEST_REPORT and RECEIVE_REPORT_ERROR mutations', done => {
|
||||
testAction(
|
||||
actions.fetchReport,
|
||||
null,
|
||||
localState,
|
||||
[
|
||||
{ type: types.REQUEST_REPORT },
|
||||
{
|
||||
type: types.RECEIVE_REPORT_ERROR,
|
||||
payload: 'Accessibility report artifact not found',
|
||||
},
|
||||
],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
actions.stopPolling();
|
||||
actions.clearEtagPoll();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
|
@ -81,7 +57,7 @@ describe('Accessibility Reports actions', () => {
|
|||
[{ type: types.REQUEST_REPORT }],
|
||||
[
|
||||
{
|
||||
payload: [{ ...data, isHead: false }, { ...data, isHead: true }],
|
||||
payload: { status: 200, data },
|
||||
type: 'receiveReportSuccess',
|
||||
},
|
||||
],
|
||||
|
@ -98,14 +74,8 @@ describe('Accessibility Reports actions', () => {
|
|||
actions.fetchReport,
|
||||
null,
|
||||
localState,
|
||||
[
|
||||
{ type: types.REQUEST_REPORT },
|
||||
{
|
||||
type: types.RECEIVE_REPORT_ERROR,
|
||||
payload: 'Failed to retrieve accessibility report',
|
||||
},
|
||||
],
|
||||
[],
|
||||
[{ type: types.REQUEST_REPORT }],
|
||||
[{ type: 'receiveReportError' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
@ -113,15 +83,39 @@ describe('Accessibility Reports actions', () => {
|
|||
});
|
||||
|
||||
describe('receiveReportSuccess', () => {
|
||||
it('should commit RECEIVE_REPORT_SUCCESS mutation', done => {
|
||||
it('should commit RECEIVE_REPORT_SUCCESS mutation with 200', done => {
|
||||
testAction(
|
||||
actions.receiveReportSuccess,
|
||||
[{ ...baseReport, isHead: false }, { ...headReport, isHead: true }],
|
||||
{ status: 200, data: mockReport },
|
||||
localState,
|
||||
[{ type: types.RECEIVE_REPORT_SUCCESS, payload: comparedReportResult }],
|
||||
[{ type: types.RECEIVE_REPORT_SUCCESS, payload: mockReport }],
|
||||
[{ type: 'stopPolling' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not commit RECEIVE_REPORTS_SUCCESS mutation with 204', done => {
|
||||
testAction(
|
||||
actions.receiveReportSuccess,
|
||||
{ status: 204, data: mockReport },
|
||||
localState,
|
||||
[],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveReportError', () => {
|
||||
it('should commit RECEIVE_REPORT_ERROR mutation', done => {
|
||||
testAction(
|
||||
actions.receiveReportError,
|
||||
null,
|
||||
localState,
|
||||
[{ type: types.RECEIVE_REPORT_ERROR }],
|
||||
[{ type: 'stopPolling' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -67,8 +67,7 @@ describe('Accessibility reports store getters', () => {
|
|||
it('returns summary message containing number of errors', () => {
|
||||
localState.report = {
|
||||
summary: {
|
||||
errors: 1,
|
||||
warnings: 1,
|
||||
errored: 2,
|
||||
},
|
||||
};
|
||||
const result = 'Accessibility scanning detected 2 issues for the source branch only';
|
||||
|
@ -81,8 +80,7 @@ describe('Accessibility reports store getters', () => {
|
|||
it('returns summary message containing no errors', () => {
|
||||
localState.report = {
|
||||
summary: {
|
||||
errors: 0,
|
||||
warnings: 0,
|
||||
errored: 0,
|
||||
},
|
||||
};
|
||||
const result = 'Accessibility scanning detected no issues for the source branch only';
|
||||
|
@ -108,7 +106,7 @@ describe('Accessibility reports store getters', () => {
|
|||
it('returns false', () => {
|
||||
localState.report = {
|
||||
status: 'success',
|
||||
summary: { errors: 0, warnings: 0 },
|
||||
summary: { errored: 0 },
|
||||
};
|
||||
|
||||
expect(getters.shouldRenderIssuesList(localState)).toEqual(false);
|
||||
|
|
|
@ -10,17 +10,12 @@ describe('Accessibility Reports mutations', () => {
|
|||
localState = localStore.state;
|
||||
});
|
||||
|
||||
describe('SET_ENDPOINTS', () => {
|
||||
it('sets base and head endpoints to give values', () => {
|
||||
const baseEndpoint = 'base_endpoint.json';
|
||||
const headEndpoint = 'head_endpoint.json';
|
||||
mutations.SET_ENDPOINTS(localState, {
|
||||
baseEndpoint,
|
||||
headEndpoint,
|
||||
});
|
||||
describe('SET_ENDPOINT', () => {
|
||||
it('sets endpoint to given value', () => {
|
||||
const endpoint = 'endpoint.json';
|
||||
mutations.SET_ENDPOINT(localState, endpoint);
|
||||
|
||||
expect(localState.baseEndpoint).toEqual(baseEndpoint);
|
||||
expect(localState.headEndpoint).toEqual(headEndpoint);
|
||||
expect(localState.endpoint).toEqual(endpoint);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -65,11 +60,5 @@ describe('Accessibility Reports mutations', () => {
|
|||
|
||||
expect(localState.hasError).toEqual(true);
|
||||
});
|
||||
|
||||
it('sets errorMessage to given message', () => {
|
||||
mutations.RECEIVE_REPORT_ERROR(localState, 'message');
|
||||
|
||||
expect(localState.errorMessage).toEqual('message');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import * as utils from '~/reports/accessibility_report/store/utils';
|
||||
import { baseReport, headReport, parsedBaseReport, comparedReportResult } from '../mock_data';
|
||||
|
||||
describe('Accessibility Report store utils', () => {
|
||||
describe('parseAccessibilityReport', () => {
|
||||
it('returns array of stringified issues', () => {
|
||||
const result = utils.parseAccessibilityReport(baseReport);
|
||||
|
||||
expect(result).toEqual(parsedBaseReport);
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareAccessibilityReports', () => {
|
||||
let reports;
|
||||
|
||||
beforeEach(() => {
|
||||
reports = [
|
||||
{
|
||||
isHead: false,
|
||||
issues: utils.parseAccessibilityReport(baseReport),
|
||||
},
|
||||
{
|
||||
isHead: true,
|
||||
issues: utils.parseAccessibilityReport(headReport),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
it('returns the comparison report with a new, resolved, and existing error', () => {
|
||||
const result = utils.compareAccessibilityReports(reports);
|
||||
|
||||
expect(result).toEqual(comparedReportResult);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -11,13 +11,14 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s
|
|||
let(:user_state) { 'active' }
|
||||
let(:ghost) { false }
|
||||
let(:user_type) { nil }
|
||||
let(:user_name) { 'Test' }
|
||||
|
||||
let!(:user) do
|
||||
users.create(id: 1,
|
||||
email: 'user@example.com',
|
||||
projects_limit: 10,
|
||||
username: 'test',
|
||||
name: 'Test',
|
||||
name: user_name,
|
||||
state: user_state,
|
||||
ghost: ghost,
|
||||
last_activity_on: 1.minute.ago,
|
||||
|
@ -232,6 +233,69 @@ describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migration, s
|
|||
|
||||
it_behaves_like 'migration_bot user commits files'
|
||||
end
|
||||
|
||||
context 'when user name is invalid' do
|
||||
let(:user_name) { '.' }
|
||||
let!(:snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: file_name, content: content) }
|
||||
let(:ids) { [4, 4] }
|
||||
|
||||
after do
|
||||
raw_repository(snippet).remove
|
||||
end
|
||||
|
||||
it_behaves_like 'migration_bot user commits files'
|
||||
end
|
||||
|
||||
context 'when both user name and snippet file_name are invalid' do
|
||||
let(:user_name) { '.' }
|
||||
let!(:other_user) do
|
||||
users.create(id: 2,
|
||||
email: 'user2@example.com',
|
||||
projects_limit: 10,
|
||||
username: 'test2',
|
||||
name: 'Test2',
|
||||
state: user_state,
|
||||
ghost: ghost,
|
||||
last_activity_on: 1.minute.ago,
|
||||
user_type: user_type,
|
||||
confirmed_at: 1.day.ago)
|
||||
end
|
||||
let!(:invalid_snippet) { snippets.create(id: 4, type: 'PersonalSnippet', author_id: user.id, file_name: '.', content: content) }
|
||||
let!(:snippet) { snippets.create(id: 5, type: 'PersonalSnippet', author_id: other_user.id, file_name: file_name, content: content) }
|
||||
let(:ids) { [4, 5] }
|
||||
|
||||
after do
|
||||
raw_repository(snippet).remove
|
||||
raw_repository(invalid_snippet).remove
|
||||
end
|
||||
|
||||
it 'updates the file_name only when it is invalid' do
|
||||
subject
|
||||
|
||||
expect(blob_at(invalid_snippet, 'snippetfile1.txt')).to be
|
||||
expect(blob_at(snippet, file_name)).to be
|
||||
end
|
||||
|
||||
it_behaves_like 'migration_bot user commits files' do
|
||||
let(:snippet) { invalid_snippet }
|
||||
end
|
||||
|
||||
it 'does not alter the commit author in subsequent migrations' do
|
||||
subject
|
||||
|
||||
last_commit = raw_repository(snippet).commit
|
||||
|
||||
expect(last_commit.author_name).to eq other_user.name
|
||||
expect(last_commit.author_email).to eq other_user.email
|
||||
end
|
||||
|
||||
it "increases the number of retries temporarily from #{described_class::MAX_RETRIES} to #{described_class::MAX_RETRIES + 1}" do
|
||||
expect(service).to receive(:create_commit).with(Snippet.find(invalid_snippet.id)).exactly(described_class::MAX_RETRIES + 1).times.and_call_original
|
||||
expect(service).to receive(:create_commit).with(Snippet.find(snippet.id)).once.and_call_original
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def blob_at(snippet, path)
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'rake_helper'
|
||||
|
||||
describe SystemCheck::App::HashedStorageAllProjectsCheck do
|
||||
before do
|
||||
silence_output
|
||||
end
|
||||
|
||||
describe '#check?' do
|
||||
it 'fails when at least one project is in legacy storage' do
|
||||
create(:project, :legacy_storage)
|
||||
|
||||
expect(subject.check?).to be_falsey
|
||||
end
|
||||
|
||||
it 'succeeds when all projects are in hashed storage' do
|
||||
create(:project)
|
||||
|
||||
expect(subject.check?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'rake_helper'
|
||||
|
||||
describe SystemCheck::App::HashedStorageEnabledCheck do
|
||||
before do
|
||||
silence_output
|
||||
end
|
||||
|
||||
describe '#check?' do
|
||||
it 'fails when hashed storage is disabled' do
|
||||
stub_application_setting(hashed_storage_enabled: false)
|
||||
|
||||
expect(subject.check?).to be_falsey
|
||||
end
|
||||
|
||||
it 'succeeds when hashed storage is enabled' do
|
||||
stub_application_setting(hashed_storage_enabled: true)
|
||||
|
||||
expect(subject.check?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
|
@ -217,6 +217,22 @@ describe SnippetRepository do
|
|||
it_behaves_like 'snippet repository with git errors', 'invalid://path/here', described_class::InvalidPathError
|
||||
it_behaves_like 'snippet repository with git errors', '../../path/traversal/here', described_class::InvalidPathError
|
||||
it_behaves_like 'snippet repository with git errors', 'README', described_class::CommitError
|
||||
|
||||
context 'when user name is invalid' do
|
||||
let(:user) { create(:user, name: '.') }
|
||||
|
||||
it_behaves_like 'snippet repository with git errors', 'non_existing_file', described_class::InvalidSignatureError
|
||||
end
|
||||
|
||||
context 'when user email is empty' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
user.update_column(:email, '')
|
||||
end
|
||||
|
||||
it_behaves_like 'snippet repository with git errors', 'non_existing_file', described_class::InvalidSignatureError
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ describe 'Getting Metrics Dashboard Annotations' do
|
|||
"description" => annotation.description,
|
||||
"id" => annotation.to_global_id.to_s,
|
||||
"panelId" => annotation.panel_xid,
|
||||
"startingAt" => annotation.starting_at.to_s,
|
||||
"startingAt" => annotation.starting_at.iso8601,
|
||||
"endingAt" => nil
|
||||
}]
|
||||
end
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require 'rubocop'
|
||||
require 'rubocop/rspec/support'
|
||||
require_relative '../../../rubocop/cop/avoid_keyword_arguments_in_sidekiq_workers'
|
||||
|
||||
describe RuboCop::Cop::AvoidKeywordArgumentsInSidekiqWorkers do
|
||||
include CopHelper
|
||||
|
||||
subject(:cop) { described_class.new }
|
||||
|
||||
it 'flags violation for keyword arguments usage in perform method signature' do
|
||||
expect_offense(<<~RUBY)
|
||||
def perform(id:)
|
||||
^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
it 'flags violation for optional keyword arguments usage in perform method signature' do
|
||||
expect_offense(<<~RUBY)
|
||||
def perform(id: nil)
|
||||
^^^^^^^^^^^^^^^^^^^^ Do not use keyword arguments in Sidekiq workers. For details, check https://github.com/mperham/sidekiq/issues/2372
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
it 'does not flag a violation for standard optional arguments usage in perform method signature' do
|
||||
expect_no_offenses(<<~RUBY)
|
||||
def perform(id = nil)
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
it 'does not flag a violation for keyword arguments usage in non-perform method signatures' do
|
||||
expect_no_offenses(<<~RUBY)
|
||||
def helper(id:)
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
it 'does not flag a violation for optional keyword arguments usage in non-perform method signatures' do
|
||||
expect_no_offenses(<<~RUBY)
|
||||
def helper(id: nil)
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
end
|
|
@ -142,7 +142,7 @@ describe Prometheus::ProxyVariableSubstitutionService do
|
|||
end
|
||||
|
||||
it_behaves_like 'success' do
|
||||
let(:expected_query) { 'up{pod_name=""}' }
|
||||
let(:expected_query) { 'up{pod_name="{{pod_name}}"}' }
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -161,28 +161,6 @@ describe Prometheus::ProxyVariableSubstitutionService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with liquid tags and ruby format variables' do
|
||||
let(:params_keys) do
|
||||
{
|
||||
query: 'up{ {% if true %}env1="%{ci_environment_slug}",' \
|
||||
'env2="{{ci_environment_slug}}"{% endif %} }'
|
||||
}
|
||||
end
|
||||
|
||||
# The following spec will fail and should be changed to a 'success' spec
|
||||
# once we remove support for the Ruby interpolation format.
|
||||
# https://gitlab.com/gitlab-org/gitlab/issues/37990
|
||||
#
|
||||
# Liquid tags `{% %}` cannot be used currently because the Ruby `%`
|
||||
# operator raises an error when it encounters a Liquid `{% %}` tag in the
|
||||
# string.
|
||||
#
|
||||
# Once we remove support for the Ruby format, users can start using
|
||||
# Liquid tags.
|
||||
|
||||
it_behaves_like 'error', 'Malformed string'
|
||||
end
|
||||
|
||||
context 'ruby template rendering' do
|
||||
let(:params_keys) do
|
||||
{ query: 'up{env=%{ci_environment_slug},%{environment_filter}}' }
|
||||
|
@ -271,17 +249,79 @@ describe Prometheus::ProxyVariableSubstitutionService do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when liquid template rendering raises error' do
|
||||
before do
|
||||
liquid_service = instance_double(TemplateEngines::LiquidService)
|
||||
context 'gsub variable substitution tolerance for weirdness' do
|
||||
context 'with whitespace around variable' do
|
||||
let(:params_keys) do
|
||||
{
|
||||
query: 'up{' \
|
||||
"env1={{ ci_environment_slug}}," \
|
||||
"env2={{ci_environment_slug }}," \
|
||||
"{{ environment_filter }}" \
|
||||
'}'
|
||||
}
|
||||
end
|
||||
|
||||
allow(TemplateEngines::LiquidService).to receive(:new).and_return(liquid_service)
|
||||
allow(liquid_service).to receive(:render).and_raise(
|
||||
TemplateEngines::LiquidService::RenderError, 'error message'
|
||||
)
|
||||
it_behaves_like 'success' do
|
||||
let(:expected_query) do
|
||||
'up{' \
|
||||
"env1=#{environment.slug}," \
|
||||
"env2=#{environment.slug}," \
|
||||
"container_name!=\"POD\",environment=\"#{environment.slug}\"" \
|
||||
'}'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'error', 'error message'
|
||||
context 'with empty variables' do
|
||||
let(:params_keys) do
|
||||
{ query: "up{env1={{}},env2={{ }}}" }
|
||||
end
|
||||
|
||||
it_behaves_like 'success' do
|
||||
let(:expected_query) { "up{env1={{}},env2={{ }}}" }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple occurrences of variable in string' do
|
||||
let(:params_keys) do
|
||||
{ query: "up{env1={{ci_environment_slug}},env2={{ci_environment_slug}}}" }
|
||||
end
|
||||
|
||||
it_behaves_like 'success' do
|
||||
let(:expected_query) { "up{env1=#{environment.slug},env2=#{environment.slug}}" }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple variables in string' do
|
||||
let(:params_keys) do
|
||||
{ query: "up{env={{ci_environment_slug}},{{environment_filter}}}" }
|
||||
end
|
||||
|
||||
it_behaves_like 'success' do
|
||||
let(:expected_query) do
|
||||
"up{env=#{environment.slug}," \
|
||||
"container_name!=\"POD\",environment=\"#{environment.slug}\"}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown variables in string' do
|
||||
let(:params_keys) { { query: "up{env={{env_slug}}}" } }
|
||||
|
||||
it_behaves_like 'success' do
|
||||
let(:expected_query) { "up{env={{env_slug}}}" }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown and known variables in string' do
|
||||
let(:params_keys) do
|
||||
{ query: "up{env={{ci_environment_slug}},other_env={{env_slug}}}" }
|
||||
end
|
||||
|
||||
it_behaves_like 'success' do
|
||||
let(:expected_query) { "up{env=#{environment.slug},other_env={{env_slug}}}" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,126 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe TemplateEngines::LiquidService do
|
||||
describe '#render' do
|
||||
let(:template) { 'up{env={{ci_environment_slug}}}' }
|
||||
let(:result) { subject }
|
||||
|
||||
let_it_be(:slug) { 'env_slug' }
|
||||
|
||||
let_it_be(:context) do
|
||||
{
|
||||
ci_environment_slug: slug,
|
||||
environment_filter: "container_name!=\"POD\",environment=\"#{slug}\""
|
||||
}
|
||||
end
|
||||
|
||||
subject { described_class.new(template).render(context) }
|
||||
|
||||
it 'with symbol keys in context it substitutes variables' do
|
||||
expect(result).to include("up{env=#{slug}")
|
||||
end
|
||||
|
||||
context 'with multiple occurrences of variable in template' do
|
||||
let(:template) do
|
||||
'up{env1={{ci_environment_slug}},env2={{ci_environment_slug}}}'
|
||||
end
|
||||
|
||||
it 'substitutes variables' do
|
||||
expect(result).to eq("up{env1=#{slug},env2=#{slug}}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with multiple variables in template' do
|
||||
let(:template) do
|
||||
'up{env={{ci_environment_slug}},' \
|
||||
'{{environment_filter}}}'
|
||||
end
|
||||
|
||||
it 'substitutes all variables' do
|
||||
expect(result).to eq(
|
||||
"up{env=#{slug}," \
|
||||
"container_name!=\"POD\",environment=\"#{slug}\"}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown variables in template' do
|
||||
let(:template) { 'up{env={{env_slug}}}' }
|
||||
|
||||
it 'does not substitute unknown variables' do
|
||||
expect(result).to eq("up{env=}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with extra variables in context' do
|
||||
let(:template) { 'up{env={{ci_environment_slug}}}' }
|
||||
|
||||
it 'substitutes variables' do
|
||||
# If context has only 1 key, there is no need for this spec.
|
||||
expect(context.count).to be > 1
|
||||
expect(result).to eq("up{env=#{slug}}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown and known variables in template' do
|
||||
let(:template) { 'up{env={{ci_environment_slug}},other_env={{env_slug}}}' }
|
||||
|
||||
it 'substitutes known variables' do
|
||||
expect(result).to eq("up{env=#{slug},other_env=}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'Liquid errors' do
|
||||
shared_examples 'raises RenderError' do |message|
|
||||
it do
|
||||
expect { result }.to raise_error(described_class::RenderError, message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when liquid raises error' do
|
||||
let(:template) { 'up{env={{ci_environment_slug}}' }
|
||||
let(:liquid_template) { Liquid::Template.new }
|
||||
|
||||
before do
|
||||
allow(Liquid::Template).to receive(:parse).with(template).and_return(liquid_template)
|
||||
allow(liquid_template).to receive(:render!).and_raise(exception, message)
|
||||
end
|
||||
|
||||
context 'raises Liquid::MemoryError' do
|
||||
let(:exception) { Liquid::MemoryError }
|
||||
let(:message) { 'Liquid error: Memory limits exceeded' }
|
||||
|
||||
it_behaves_like 'raises RenderError', 'Memory limit exceeded while rendering template'
|
||||
end
|
||||
|
||||
context 'raises Liquid::Error' do
|
||||
let(:exception) { Liquid::Error }
|
||||
let(:message) { 'Liquid error: Generic error message' }
|
||||
|
||||
it_behaves_like 'raises RenderError', 'Error rendering query'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with template that is expensive to render' do
|
||||
let(:template) do
|
||||
'{% assign loop_count = 1000 %}'\
|
||||
'{% assign padStr = "0" %}'\
|
||||
'{% assign number_to_pad = "1" %}'\
|
||||
'{% assign strLength = number_to_pad | size %}'\
|
||||
'{% assign padLength = loop_count | minus: strLength %}'\
|
||||
'{% if padLength > 0 %}'\
|
||||
' {% assign padded = number_to_pad %}'\
|
||||
' {% for position in (1..padLength) %}'\
|
||||
' {% assign padded = padded | prepend: padStr %}'\
|
||||
' {% endfor %}'\
|
||||
' {{ padded }}'\
|
||||
'{% endif %}'
|
||||
end
|
||||
|
||||
it_behaves_like 'raises RenderError', 'Memory limit exceeded while rendering template'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,6 +17,25 @@ describe CreateCommitSignatureWorker do
|
|||
subject { described_class.new.perform(commit_shas, project.id) }
|
||||
|
||||
context 'when a signature is found' do
|
||||
it_behaves_like 'an idempotent worker' do
|
||||
let(:job_args) { [commit_shas, project.id] }
|
||||
|
||||
before do
|
||||
# Removing the stub which can cause bugs for multiple calls to
|
||||
# Project#commits_by.
|
||||
allow(project).to receive(:commits_by).and_call_original
|
||||
|
||||
# Making sure it still goes through all the perform execution.
|
||||
allow_next_instance_of(::Commit) do |commit|
|
||||
allow(commit).to receive(:signature_type).and_return(:PGP)
|
||||
end
|
||||
|
||||
allow_next_instance_of(::Gitlab::Gpg::Commit) do |gpg|
|
||||
expect(gpg).to receive(:signature).once.and_call_original
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'calls Gitlab::Gpg::Commit#signature' do
|
||||
commits.each do |commit|
|
||||
allow(commit).to receive(:signature_type).and_return(:PGP)
|
||||
|
|