Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-05-05 18:08:40 +00:00
parent dad16033c2
commit 0b789f95a3
58 changed files with 589 additions and 178 deletions

View file

@ -1 +1 @@
1f98d5a94c880e3e556ae3ace095f83e44f002fb
86aa7ee82a5dd241fd7d4b33435da0a7ecad12b0

View file

@ -0,0 +1,51 @@
<script>
import { GlListbox } from '@gitlab/ui';
import { s__ } from '~/locale';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
export default {
name: 'BackgroundMigrationsDatabaseListbox',
i18n: {
database: s__('BackgroundMigrations|Database'),
},
components: {
GlListbox,
},
props: {
databases: {
type: Array,
required: true,
},
selectedDatabase: {
type: String,
required: true,
},
},
data() {
return {
selected: this.selectedDatabase,
};
},
methods: {
selectDatabase(database) {
visitUrl(setUrlParams({ database }));
},
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center" data-testid="database-listbox">
<label id="label" class="gl-font-weight-bold gl-mr-4 gl-mb-0">{{
$options.i18n.database
}}</label>
<gl-listbox
v-model="selected"
:items="databases"
right
:toggle-text="selectedDatabase"
aria-labelledby="label"
@select="selectDatabase"
/>
</div>
</template>

View file

@ -0,0 +1,38 @@
import Vue from 'vue';
import * as Sentry from '@sentry/browser';
import Translate from '~/vue_shared/translate';
import BackgroundMigrationsDatabaseListbox from './components/database_listbox.vue';
Vue.use(Translate);
export const initBackgroundMigrationsApp = () => {
const el = document.getElementById('js-database-listbox');
if (!el) {
return false;
}
const { selectedDatabase } = el.dataset;
let { databases } = el.dataset;
try {
databases = JSON.parse(databases).map((database) => ({
value: database,
text: database,
}));
} catch (e) {
Sentry.captureException(e);
}
return new Vue({
el,
render(createElement) {
return createElement(BackgroundMigrationsDatabaseListbox, {
props: {
databases,
selectedDatabase,
},
});
},
});
};

View file

@ -72,7 +72,7 @@ export default {
async applySelectedLanguage(language) {
this.selectedLanguage = language;
await codeBlockLanguageLoader.loadLanguages([language.syntax]);
await codeBlockLanguageLoader.loadLanguage(language.syntax);
this.tiptapEditor.commands.setCodeBlock({ language: this.selectedLanguage.syntax });
},

View file

@ -1,9 +1,10 @@
<script>
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { __ } from '~/locale';
import codeBlockLanguageLoader from '../../services/code_block_language_loader';
export default {
name: 'FrontMatter',
name: 'CodeBlock',
components: {
NodeViewWrapper,
NodeViewContent,
@ -13,6 +14,16 @@ export default {
type: Object,
required: true,
},
updateAttributes: {
type: Function,
required: true,
},
},
async mounted() {
const lang = codeBlockLanguageLoader.findLanguageBySyntax(this.node.attrs.language);
await codeBlockLanguageLoader.loadLanguage(lang.syntax);
this.updateAttributes({ language: this.node.attrs.language });
},
i18n: {
frontmatter: __('frontmatter'),
@ -22,6 +33,7 @@ export default {
<template>
<node-view-wrapper class="content-editor-code-block gl-relative code highlight" as="pre">
<span
v-if="node.attrs.isFrontmatter"
data-testid="frontmatter-label"
class="gl-absolute gl-top-0 gl-right-3"
contenteditable="false"

View file

@ -1,6 +1,8 @@
import { CodeBlockLowlight } from '@tiptap/extension-code-block-lowlight';
import { textblockTypeInputRule } from '@tiptap/core';
import codeBlockLanguageLoader from '../services/code_block_language_loader';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import languageLoader from '../services/code_block_language_loader';
import CodeBlockWrapper from '../components/wrappers/code_block.vue';
const extractLanguage = (element) => element.getAttribute('lang');
export const backtickInputRegex = /^```([a-z]+)?[\s\n]$/;
@ -9,14 +11,6 @@ export const tildeInputRegex = /^~~~([a-z]+)?[\s\n]$/;
export default CodeBlockLowlight.extend({
isolating: true,
exitOnArrowDown: false,
addOptions() {
return {
...this.parent?.(),
languageLoader: codeBlockLanguageLoader,
};
},
addAttributes() {
return {
language: {
@ -30,7 +24,6 @@ export default CodeBlockLowlight.extend({
};
},
addInputRules() {
const { languageLoader } = this.options;
const getAttributes = (match) => languageLoader?.loadLanguageFromInputRule(match) || {};
return [
@ -65,4 +58,8 @@ export default CodeBlockLowlight.extend({
['code', {}, 0],
];
},
addNodeView() {
return new VueNodeViewRenderer(CodeBlockWrapper);
},
});

View file

@ -14,6 +14,9 @@ export default CodeBlockHighlight.extend({
return element.dataset.diagram;
},
},
isDiagram: {
default: true,
},
};
},

View file

@ -1,10 +1,18 @@
import { VueNodeViewRenderer } from '@tiptap/vue-2';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import FrontmatterWrapper from '../components/wrappers/frontmatter.vue';
import CodeBlockHighlight from './code_block_highlight';
export default CodeBlockHighlight.extend({
name: 'frontmatter',
addAttributes() {
return {
...this.parent?.(),
isFrontmatter: {
default: true,
},
};
},
parseHTML() {
return [
{
@ -24,9 +32,6 @@ export default CodeBlockHighlight.extend({
},
};
},
addNodeView() {
return new VueNodeViewRenderer(FrontmatterWrapper);
},
addInputRules() {
return [];

View file

@ -40,25 +40,21 @@ const codeBlockLanguageLoader = {
loadLanguageFromInputRule(match) {
const { syntax } = this.findLanguageBySyntax(match[1]);
this.loadLanguages([syntax]);
this.loadLanguage(syntax);
return { language: syntax };
},
loadLanguages(languageList = []) {
const loaders = languageList
.filter(
(languageName) => !this.isLanguageLoaded(languageName) && languageName in languageLoader,
)
.map((languageName) => {
return languageLoader[languageName]()
.then(({ default: language }) => {
this.lowlight.registerLanguage(languageName, language);
})
.catch(() => false);
});
async loadLanguage(languageName) {
if (this.isLanguageLoaded(languageName)) return false;
return Promise.all(loaders);
try {
const { default: language } = await languageLoader[languageName]();
this.lowlight.registerLanguage(languageName, language);
return true;
} catch {
return false;
}
},
};

View file

@ -3,12 +3,11 @@ import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } fro
/* eslint-disable no-underscore-dangle */
export class ContentEditor {
constructor({ tiptapEditor, serializer, deserializer, assetResolver, eventHub, languageLoader }) {
constructor({ tiptapEditor, serializer, deserializer, assetResolver, eventHub }) {
this._tiptapEditor = tiptapEditor;
this._serializer = serializer;
this._deserializer = deserializer;
this._eventHub = eventHub;
this._languageLoader = languageLoader;
this._assetResolver = assetResolver;
}
@ -49,7 +48,7 @@ export class ContentEditor {
}
async setSerializedContent(serializedContent) {
const { _tiptapEditor: editor, _eventHub: eventHub, _languageLoader: languageLoader } = this;
const { _tiptapEditor: editor, _eventHub: eventHub } = this;
const { doc, tr } = editor.state;
const selection = TextSelection.create(doc, 0, doc.content.size);
@ -58,12 +57,8 @@ export class ContentEditor {
const result = await this.deserialize(serializedContent);
if (Object.keys(result).length !== 0) {
const { document, languages } = result;
await languageLoader.loadLanguages(languages);
tr.setSelection(selection)
.replaceSelectionWith(document, false)
.replaceSelectionWith(result.document, false)
.setMeta('preventUpdate', true);
editor.view.dispatch(tr);
}

View file

@ -62,7 +62,6 @@ import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
import createRemarkMarkdownDeserializer from './remark_markdown_deserializer';
import createAssetResolver from './asset_resolver';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
import languageLoader from './code_block_language_loader';
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
new Editor({
@ -96,7 +95,7 @@ export const createContentEditor = ({
BulletList,
Code,
ColorChip,
CodeBlockHighlight.configure({ lowlight, languageLoader }),
CodeBlockHighlight.configure({ lowlight }),
DescriptionItem,
DescriptionList,
Details,
@ -160,7 +159,6 @@ export const createContentEditor = ({
serializer,
eventHub,
deserializer,
languageLoader,
assetResolver,
});
};

View file

@ -18,7 +18,6 @@ export default ({ render }) => {
return {
deserialize: async ({ schema, content }) => {
const html = await render(content);
const languages = [];
if (!html) return {};
@ -28,11 +27,7 @@ export default ({ render }) => {
// append original source as a comment that nodes can access
body.append(document.createComment(content));
body.querySelectorAll('pre').forEach((preElement) => {
languages.push(preElement.getAttribute('lang'));
});
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body), languages };
return { document: ProseMirrorDOMParser.fromSchema(schema).parse(body) };
},
};
};

View file

@ -35,10 +35,10 @@ const factorySpecs = {
pre: {
block: 'codeBlock',
skipChildren: true,
getContent: ({ hastNodeText }) => hastNodeText,
getContent: ({ hastNodeText }) => hastNodeText.replace(/\n$/, ''),
getAttrs: (hastNode) => {
const languageClass = hastNode.children[0]?.properties.className?.[0];
const language = isString(languageClass) ? languageClass.replace('language-', '') : '';
const language = isString(languageClass) ? languageClass.replace('language-', '') : null;
return { language };
},
@ -81,7 +81,7 @@ export default () => {
}),
});
return { document, languages: [] };
return { document };
},
};
};

View file

@ -170,23 +170,6 @@
}
]
},
"cobertura": {
"description": "Path for file(s) that should be parsed as Cobertura XML coverage report",
"oneOf": [
{
"type": "string",
"description": "Path to a single XML file"
},
{
"type": "array",
"description": "A list of paths to XML files that will automatically be merged into one report",
"items": {
"type": "string"
},
"minItems": 1
}
]
},
"coverage_report": {
"type": "object",
"description": "Used to collect coverage reports from the job.",

View file

@ -0,0 +1,3 @@
import { initBackgroundMigrationsApp } from '~/admin/background_migrations';
initBackgroundMigrationsApp();

View file

@ -54,7 +54,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def service_usage_data
@service_ping_data_present = Rails.cache.exist?('usage_data')
@service_ping_data_present = prerecorded_service_ping_data.present?
end
def update
@ -64,7 +64,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def usage_data
respond_to do |format|
format.html do
usage_data_json = Gitlab::Json.pretty_generate(Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values, cached: true))
usage_data_json = Gitlab::Json.pretty_generate(service_ping_data)
render html: Gitlab::Highlight.highlight('payload.json', usage_data_json, language: 'json')
end
@ -72,7 +72,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
format.json do
Gitlab::UsageDataCounters::ServiceUsageDataCounter.count(:download_payload_click)
render json: Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values, cached: true).to_json
render json: service_ping_data.to_json
end
end
end
@ -307,6 +307,14 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def valid_setting_panels
VALID_SETTING_PANELS
end
def service_ping_data
prerecorded_service_ping_data || Gitlab::Usage::ServicePingReport.for(output: :all_metrics_values)
end
def prerecorded_service_ping_data
Rails.cache.fetch(Gitlab::Usage::ServicePingReport::CACHE_KEY) || ::RawUsageData.for_current_reporting_cycle.first&.payload
end
end
Admin::ApplicationSettingsController.prepend_mod_with('Admin::ApplicationSettingsController')

View file

@ -53,9 +53,9 @@ class Admin::BackgroundMigrationsController < Admin::ApplicationController
end
def base_model
database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
@selected_database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
Gitlab::Database.database_base_models[database]
Gitlab::Database.database_base_models[@selected_database]
end
def batched_migration_class

View file

@ -21,6 +21,7 @@ module Types
field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.'
field :tags_count, GraphQL::Types::Int, null: false, description: 'Number of tags associated with this image.'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.'
field :last_cleanup_deleted_tags_count, GraphQL::Types::Int, null: true, description: 'Number of deleted tags from the last cleanup.'
def can_delete
Ability.allowed?(current_user, :update_container_image, object)

View file

@ -1,9 +1,16 @@
# frozen_string_literal: true
class RawUsageData < ApplicationRecord
REPORTING_CADENCE = 7.days.freeze
validates :payload, presence: true
validates :recorded_at, presence: true, uniqueness: true
scope :for_current_reporting_cycle, -> do
where('created_at >= ?', REPORTING_CADENCE.ago.beginning_of_day)
.order(created_at: :desc)
end
def update_version_metadata!(usage_data_id:)
self.update_columns(sent_at: Time.current, version_usage_data_id_value: usage_data_id)
end

View file

@ -12,14 +12,14 @@
= gl_badge_tag migration.status_name.to_s.humanize, { size: :sm, variant: batched_migration_status_badge_variant(migration) }
%td{ role: 'cell', data: { label: _('Action') } }
- if migration.active?
= button_to pause_admin_background_migration_path(migration),
= button_to pause_admin_background_migration_path(migration, database: params[:database]),
class: 'gl-button btn btn-icon has-tooltip', title: _('Pause'), 'aria-label' => _('Pause') do
= sprite_icon('pause', css_class: 'gl-button-icon gl-icon')
- elsif migration.paused?
= button_to resume_admin_background_migration_path(migration),
= button_to resume_admin_background_migration_path(migration, database: params[:database]),
class: 'gl-button btn btn-icon has-tooltip', title: _('Resume'), 'aria-label' => _('Resume') do
= sprite_icon('play', css_class: 'gl-button-icon gl-icon')
- elsif migration.failed?
= button_to retry_admin_background_migration_path(migration),
= button_to retry_admin_background_migration_path(migration, database: params[:database]),
class: 'gl-button btn btn-icon has-tooltip', title: _('Retry'), 'aria-label' => _('Retry') do
= sprite_icon('retry', css_class: 'gl-button-icon gl-icon')

View file

@ -1,13 +1,25 @@
- page_title _('Background Migrations')
- page_title s_('BackgroundMigrations|Background Migrations')
.gl-display-flex.gl-sm-flex-direction-column.gl-sm-align-items-flex-end.gl-pb-5.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-100
.gl-flex-grow-1.gl-mr-7
%h3= s_('BackgroundMigrations|Background Migrations')
%p.light.gl-mb-0
- learnmore_link = help_page_path('development/database/batched_background_migrations')
- learnmore_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: learnmore_link }
= html_escape(s_('BackgroundMigrations|Background migrations are used to perform data migrations whenever a migration exceeds the time limits in our guidelines. %{linkStart}Learn more%{linkEnd}')) % { linkStart: learnmore_link_start, linkEnd: '</a>'.html_safe }
- if @databases.size > 1
.gl-display-flex.gl-align-items-center.gl-flex-grow-0.gl-flex-basis-0.gl-sm-mt-0.gl-mt-5
#js-database-listbox{ data: { databases: @databases, selected_database: @selected_database } }
= gl_tabs_nav do
= gl_tab_link_to admin_background_migrations_path, item_active: @current_tab == 'queued' do
= gl_tab_link_to admin_background_migrations_path({ tab: nil, database: params[:database] }), item_active: @current_tab == 'queued' do
= _('Queued')
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['queued'])
= gl_tab_link_to admin_background_migrations_path(tab: 'failed'), item_active: @current_tab == 'failed' do
= gl_tab_link_to admin_background_migrations_path({ tab: 'failed', database: params[:database] }), item_active: @current_tab == 'failed' do
= _('Failed')
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['failed'])
= gl_tab_link_to admin_background_migrations_path(tab: 'finished'), item_active: @current_tab == 'finished' do
= gl_tab_link_to admin_background_migrations_path({ tab: 'finished', database: params[:database] }), item_active: @current_tab == 'finished' do
= _('Finished')
= gl_tab_counter_badge limited_counter_with_delimiter(@relations_by_tab['finished'])

View file

@ -9833,6 +9833,7 @@ A container repository.
| <a id="containerrepositoryexpirationpolicycleanupstatus"></a>`expirationPolicyCleanupStatus` | [`ContainerRepositoryCleanupStatus`](#containerrepositorycleanupstatus) | Tags cleanup status for the container repository. |
| <a id="containerrepositoryexpirationpolicystartedat"></a>`expirationPolicyStartedAt` | [`Time`](#time) | Timestamp when the cleanup done by the expiration policy was started on the container repository. |
| <a id="containerrepositoryid"></a>`id` | [`ID!`](#id) | ID of the container repository. |
| <a id="containerrepositorylastcleanupdeletedtagscount"></a>`lastCleanupDeletedTagsCount` | [`Int`](#int) | Number of deleted tags from the last cleanup. |
| <a id="containerrepositorylocation"></a>`location` | [`String!`](#string) | URL of the container repository. |
| <a id="containerrepositorymigrationstate"></a>`migrationState` | [`String!`](#string) | Migration state of the container repository. |
| <a id="containerrepositoryname"></a>`name` | [`String!`](#string) | Name of the container repository. |
@ -9855,6 +9856,7 @@ Details of a container repository.
| <a id="containerrepositorydetailsexpirationpolicycleanupstatus"></a>`expirationPolicyCleanupStatus` | [`ContainerRepositoryCleanupStatus`](#containerrepositorycleanupstatus) | Tags cleanup status for the container repository. |
| <a id="containerrepositorydetailsexpirationpolicystartedat"></a>`expirationPolicyStartedAt` | [`Time`](#time) | Timestamp when the cleanup done by the expiration policy was started on the container repository. |
| <a id="containerrepositorydetailsid"></a>`id` | [`ID!`](#id) | ID of the container repository. |
| <a id="containerrepositorydetailslastcleanupdeletedtagscount"></a>`lastCleanupDeletedTagsCount` | [`Int`](#int) | Number of deleted tags from the last cleanup. |
| <a id="containerrepositorydetailslocation"></a>`location` | [`String!`](#string) | URL of the container repository. |
| <a id="containerrepositorydetailsmigrationstate"></a>`migrationState` | [`String!`](#string) | Migration state of the container repository. |
| <a id="containerrepositorydetailsname"></a>`name` | [`String!`](#string) | Name of the container repository. |

View file

@ -80,33 +80,16 @@ GitLab can display the results of one or more reports in:
- The [security dashboard](../../user/application_security/security_dashboard/index.md).
- The [Project Vulnerability report](../../user/application_security/vulnerability_report/index.md).
## `artifacts:reports:cobertura` (DEPRECATED)
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3708) in GitLab 12.9.
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78132) in GitLab 14.9.
WARNING:
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78132)
in GitLab 14.9 and planned for removal in GitLab 15.0. The alternative `artifacts:reports:coverage_report`
is available GitLab 14.10.
The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md).
The collected Cobertura coverage reports upload to GitLab as an artifact.
GitLab can display the results of one or more reports in the merge request
[diff annotations](../../user/project/merge_requests/test_coverage_visualization.md).
Cobertura was originally developed for Java, but there are many third-party ports for other languages such as
JavaScript, Python, and Ruby.
## `artifacts:reports:coverage_report`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/344533) in GitLab 14.10.
Use `coverage_report` to collect coverage report in Cobertura format, similar to `artifacts:reports:cobertura`.
Use `coverage_report` to collect coverage report in Cobertura format.
NOTE:
`artifacts:reports:coverage_report` cannot be used at the same time with `artifacts:reports:cobertura`.
The `cobertura` report collects [Cobertura coverage XML files](../../user/project/merge_requests/test_coverage_visualization.md).
Cobertura was originally developed for Java, but there are many third-party ports for other languages such as
JavaScript, Python, and Ruby.
```yaml
artifacts:

View file

@ -698,7 +698,7 @@ Properties of customer critical merge requests:
- It is required that the reviewer(s) and maintainer(s) involved with a customer critical merge request are engaged as soon as this decision is made.
- It is required to prioritize work for those involved on a customer critical merge request so that they have the time available necessary to focus on it.
- It is required to adhere to GitLab [values](https://about.gitlab.com/handbook/values/) and processes when working on customer critical merge requests, taking particular note of family and friends first/work second, definition of done, iteration, and release when it's ready.
- Customer critical merge requests are required to not reduce security, introduce data-loss risk, reduce availability, nor break existing functionality per the process for [prioritizing technical decisions](https://about.gitlab.com/handbook/engineering/principles/#prioritizing-technical-decisions).
- Customer critical merge requests are required to not reduce security, introduce data-loss risk, reduce availability, nor break existing functionality per the process for [prioritizing technical decisions](https://about.gitlab.com/handbook/engineering/development/principles/#prioritizing-technical-decisions).
- On customer critical requests, it is _recommended_ that those involved _consider_ coordinating synchronously (Zoom, Slack) in addition to asynchronously (merge requests comments) if they believe this may reduce the elapsed time to merge even though this _may_ sacrifice [efficiency](https://about.gitlab.com/company/culture/all-remote/asynchronous/#evaluating-efficiency.md).
- After a customer critical merge request is merged, a retrospective must be completed with the intention of reducing the frequency of future customer critical merge requests.

View file

@ -66,7 +66,7 @@ continue to apply. However, there are a few things that deserve special emphasis
Danger is a powerful tool and flexible tool, but not always the most appropriate
way to solve a given problem or workflow.
First, be aware of the GitLab [commitment to dogfooding](https://about.gitlab.com/handbook/engineering/principles/#dogfooding).
First, be aware of the GitLab [commitment to dogfooding](https://about.gitlab.com/handbook/engineering/development/principles/#dogfooding).
The code we write for Danger is GitLab-specific, and it **may not** be most
appropriate place to implement functionality that addresses a need we encounter.
Our users, customers, and even our own satellite projects, such as [Gitaly](https://gitlab.com/gitlab-org/gitaly),

View file

@ -539,7 +539,7 @@ When writing about licenses:
- Do not use variations such as **cloud license**, **offline license**, or **legacy license**.
- Do not use interchangeably with **subscription**:
- A license grants users access to the subscription they purchased, and contains information such as the number of seats and subscription dates.
- A license grants users access to the subscription they purchased, and contains information such as the number of seats they purchased and subscription dates.
- A subscription is the subscription tier that the user purchases.
Use:

View file

@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Anti-patterns may seem like good approaches at first, but it has been shown that they bring more ills than benefits. These should
generally be avoided.
Throughout the GitLab codebase, there may be historic uses of these anti-patterns. Please [use discretion](https://about.gitlab.com/handbook/engineering/principles/#balance-refactoring-and-velocity)
Throughout the GitLab codebase, there may be historic uses of these anti-patterns. Please [use discretion](https://about.gitlab.com/handbook/engineering/development/principles/#balance-refactoring-and-velocity)
when figuring out whether or not to refactor, when touching code that uses one of these legacy patterns.
NOTE:

View file

@ -98,6 +98,20 @@ virtual machine:
fips-mode-setup --disable
```
#### Detect FIPS enablement in code
You can query `GitLab::FIPS` in Ruby code to determine if the instance is FIPS-enabled:
```ruby
def default_min_key_size(name)
if Gitlab::FIPS.enabled?
Gitlab::SSHPublicKey.supported_sizes(name).select(&:positive?).min || -1
else
0
end
end
```
## Set up a FIPS-enabled cluster
You can use the [GitLab Environment Toolkit](https://gitlab.com/gitlab-org/gitlab-environment-toolkit) to spin

View file

@ -12,7 +12,7 @@ which itself includes files under
[`.gitlab/ci/`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/.gitlab/ci)
for easier maintenance.
We're striving to [dogfood](https://about.gitlab.com/handbook/engineering/principles/#dogfooding)
We're striving to [dogfood](https://about.gitlab.com/handbook/engineering/development/principles/#dogfooding)
GitLab [CI/CD features and best-practices](../ci/yaml/index.md)
as much as possible.

View file

@ -272,4 +272,4 @@ and merged back independently.
- **Give yourself enough time to fix problems ahead of a milestone release.** GitLab moves fast.
As a Ruby upgrade requires many MRs to be sent and reviewed, make sure all changes are merged at least a week
before the 22nd. This gives us extra time to act if something breaks. If in doubt, it is better to
postpone the upgrade to the following month, as we [prioritize availability over velocity](https://about.gitlab.com/handbook/engineering/principles/#prioritizing-technical-decisions).
postpone the upgrade to the following month, as we [prioritize availability over velocity](https://about.gitlab.com/handbook/engineering/development/principles/#prioritizing-technical-decisions).

View file

@ -289,6 +289,8 @@ Enabled by default in GitLab 13.7 and later.
To increment the values, the related feature `usage_data_<event_name>` must be enabled.
Feature flags are required for this API and they can't be removed, they can be set to `default_enabled: true`.
```plaintext
POST /usage_data/increment_counter
```

View file

@ -214,7 +214,7 @@ apply more than one:
```shell
omnibus_gitconfig['system'] = {
# Set the http.postBuffer size, in bytes
"http" => ["postBuffer => 524288000"]
"http" => ["postBuffer = 524288000"]
}
```

View file

@ -28,7 +28,7 @@ between pipeline completion and the visualization loading on the page.
For the coverage analysis to work, you have to provide a properly formatted
[Cobertura XML](https://cobertura.github.io/cobertura/) report to
[`artifacts:reports:cobertura`](../../../ci/yaml/artifacts_reports.md#artifactsreportscobertura-deprecated).
[`artifacts:reports:coverage_report`](../../../ci/yaml/artifacts_reports.md#artifactsreportscoverage_report).
This format was originally developed for Java, but most coverage analysis frameworks
for other languages have plugins to add support for it, like:

View file

@ -15,7 +15,7 @@ module Gitlab
ALLOWED_KEYS =
%i[junit codequality sast secret_detection dependency_scanning container_scanning
dast performance browser_performance load_performance license_scanning metrics lsif
dotenv cobertura terraform accessibility
dotenv terraform accessibility
requirements coverage_fuzzing api_fuzzing cluster_image_scanning
coverage_report].freeze
@ -45,13 +45,10 @@ module Gitlab
validates :metrics, array_of_strings_or_string: true
validates :lsif, array_of_strings_or_string: true
validates :dotenv, array_of_strings_or_string: true
validates :cobertura, array_of_strings_or_string: true
validates :terraform, array_of_strings_or_string: true
validates :accessibility, array_of_strings_or_string: true
validates :requirements, array_of_strings_or_string: true
end
validates :config, mutually_exclusive_keys: [:coverage_report, :cobertura]
end
def value

View file

@ -84,7 +84,9 @@ test_artifacts:
artifacts:
reports:
junit: "./artifacts/results.xml"
cobertura: "./artifacts/cobertura.xml"
coverage_report:
coverage_format: cobertura
path: "./artifacts/cobertura.xml"
paths:
- "./artifacts"

View file

@ -3,6 +3,8 @@
module Gitlab
module Usage
class ServicePingReport
CACHE_KEY = 'usage_data'
class << self
def for(output:, cached: false)
case output.to_sym
@ -26,7 +28,7 @@ module Gitlab
end
def all_metrics_values(cached)
Rails.cache.fetch('usage_data', force: !cached, expires_in: 2.weeks) do
Rails.cache.fetch(CACHE_KEY, force: !cached, expires_in: 2.weeks) do
Gitlab::UsageData.data
end
end

View file

@ -5545,6 +5545,15 @@ msgstr ""
msgid "Background color"
msgstr ""
msgid "BackgroundMigrations|Background Migrations"
msgstr ""
msgid "BackgroundMigrations|Background migrations are used to perform data migrations whenever a migration exceeds the time limits in our guidelines. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "BackgroundMigrations|Database"
msgstr ""
msgid "Badges"
msgstr ""
@ -11327,9 +11336,18 @@ msgstr ""
msgid "DastProfiles|New site profile"
msgstr ""
msgid "DastProfiles|No scanner profile selected"
msgstr ""
msgid "DastProfiles|No scanner profile selected."
msgstr ""
msgid "DastProfiles|No scanner profiles created yet"
msgstr ""
msgid "DastProfiles|No site profile selected"
msgstr ""
msgid "DastProfiles|No site profiles created yet"
msgstr ""
@ -11378,9 +11396,27 @@ msgstr ""
msgid "DastProfiles|Scanner name"
msgstr ""
msgid "DastProfiles|Scanner profile"
msgstr ""
msgid "DastProfiles|Scanner profiles define the configuration details of a security scanner. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "DastProfiles|Select a scanner profile to run a DAST scan"
msgstr ""
msgid "DastProfiles|Select a site profile to run a DAST scan"
msgstr ""
msgid "DastProfiles|Select branch"
msgstr ""
msgid "DastProfiles|Select scanner profile"
msgstr ""
msgid "DastProfiles|Select site profile"
msgstr ""
msgid "DastProfiles|Show debug messages"
msgstr ""
@ -11393,6 +11429,9 @@ msgstr ""
msgid "DastProfiles|Site name"
msgstr ""
msgid "DastProfiles|Site profiles define the attributes and configuration details of your deployed application, website, or API. %{linkStart}Learn more%{linkEnd}."
msgstr ""
msgid "DastProfiles|Site type"
msgstr ""

View file

@ -29,7 +29,8 @@ module QA
it(
'is determined based on forward:pipeline_variables condition',
:aggregate_failures,
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/360745'
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/360745',
quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/361400', type: :investigating }
) do
# Is inheritable when true
expect(child1_pipeline).to have_variable(key: key, value: value),

View file

@ -66,6 +66,26 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
sign_in(admin)
end
context 'when there are recent ServicePing reports' do
it 'attempts to use prerecorded data' do
create(:raw_usage_data)
expect(Gitlab::Usage::ServicePingReport).not_to receive(:for)
get :usage_data, format: :json
end
end
context 'when there are NO recent ServicePing reports' do
it 'calculates data on the fly' do
allow(Gitlab::Usage::ServicePingReport).to receive(:for).and_call_original
get :usage_data, format: :json
expect(Gitlab::Usage::ServicePingReport).to have_received(:for)
end
end
it 'returns HTML data' do
get :usage_data, format: :html
@ -368,4 +388,37 @@ RSpec.describe Admin::ApplicationSettingsController, :do_not_mock_admin_mode_set
expect(response).to redirect_to("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf")
end
end
describe 'GET #service_usage_data' do
before do
stub_usage_data_connections
stub_database_flavor_check
sign_in(admin)
end
it 'assigns truthy value if there are recent ServicePing reports in database' do
create(:raw_usage_data)
get :service_usage_data, format: :html
expect(assigns(:service_ping_data_present)).to be_truthy
expect(response).to have_gitlab_http_status(:ok)
end
it 'assigns truthy value if there are recent ServicePing reports in cache', :use_clean_rails_memory_store_caching do
Rails.cache.write('usage_data', true)
get :service_usage_data, format: :html
expect(assigns(:service_ping_data_present)).to be_truthy
expect(response).to have_gitlab_http_status(:ok)
end
it 'assigns falsey value if there are NO recent ServicePing reports' do
get :service_usage_data, format: :html
expect(assigns(:service_ping_data_present)).to be_falsey
expect(response).to have_gitlab_http_status(:ok)
end
end
end

View file

@ -77,6 +77,17 @@ RSpec.describe "Admin > Admin sees background migrations" do
end
end
it 'can fire an action with a database param' do
visit admin_background_migrations_path(database: 'main')
within '#content-body' do
tab = find_link 'Failed'
tab.click
expect(page).to have_selector("[method='post'][action='/admin/background_migrations/#{failed_migration.id}/retry?database=main']")
end
end
it 'can view and retry them' do
visit admin_background_migrations_path
@ -120,4 +131,83 @@ RSpec.describe "Admin > Admin sees background migrations" do
expect(page).to have_content(finished_migration.status_name.to_s)
end
end
it 'can change tabs and retain database param' do
visit admin_background_migrations_path(database: 'ci')
within '#content-body' do
tab = find_link 'Finished'
expect(tab[:class]).not_to include('gl-tab-nav-item-active')
tab.click
expect(page).to have_current_path(admin_background_migrations_path(tab: 'finished', database: 'ci'))
expect(tab[:class]).to include('gl-tab-nav-item-active')
end
end
it 'can view documentation from Learn more link' do
visit admin_background_migrations_path
within '#content-body' do
expect(page).to have_link('Learn more', href: help_page_path('development/database/batched_background_migrations'))
end
end
describe 'selected database toggle', :js do
context 'when multi database is not enabled' do
before do
allow(Gitlab::Database).to receive(:db_config_names).and_return(['main'])
end
it 'does not render the database listbox' do
visit admin_background_migrations_path
expect(page).not_to have_selector('[data-testid="database-listbox"]')
end
end
context 'when multi database is enabled' do
before do
allow(Gitlab::Database).to receive(:db_config_names).and_return(%w[main ci])
end
it 'does render the database listbox' do
visit admin_background_migrations_path
expect(page).to have_selector('[data-testid="database-listbox"]')
end
it 'defaults to main when no parameter is passed' do
visit admin_background_migrations_path
listbox = page.find('[data-testid="database-listbox"]')
expect(listbox).to have_text('main')
end
it 'shows correct database when a parameter is passed' do
visit admin_background_migrations_path(database: 'ci')
listbox = page.find('[data-testid="database-listbox"]')
expect(listbox).to have_text('ci')
end
it 'updates the path to correct database when clicking on listbox option' do
visit admin_background_migrations_path
listbox = page.find('[data-testid="database-listbox"]')
expect(listbox).to have_text('main')
listbox.find('button').click
listbox.find('li', text: 'ci').click
wait_for_requests
expect(page).to have_current_path(admin_background_migrations_path(database: 'ci'))
listbox = page.find('[data-testid="database-listbox"]')
expect(listbox).to have_text('ci')
end
end
end
end

View file

@ -834,10 +834,9 @@ RSpec.describe 'Admin updates settings' do
stub_database_flavor_check
end
context 'when service data cached', :clean_gitlab_redis_cache do
context 'when service data cached', :use_clean_rails_memory_store_caching do
before do
allow(Rails.cache).to receive(:exist?).with('usage_data').and_return(true)
visit usage_data_admin_application_settings_path
visit service_usage_data_admin_application_settings_path
end

View file

@ -1,6 +1,6 @@
{
"type": "object",
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "expirationPolicyCleanupStatus", "project"],
"required": ["id", "name", "path", "location", "createdAt", "updatedAt", "tagsCount", "canDelete", "expirationPolicyCleanupStatus", "project", "lastCleanupDeletedTagsCount"],
"properties": {
"id": {
"type": "string"
@ -38,6 +38,9 @@
},
"project": {
"type": "object"
},
"lastCleanupDeletedTagsCount": {
"type": ["string", "null"]
}
}
}

View file

@ -0,0 +1,57 @@
import { GlListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import BackgroundMigrationsDatabaseListbox from '~/admin/background_migrations/components/database_listbox.vue';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { MOCK_DATABASES, MOCK_SELECTED_DATABASE } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
setUrlParams: jest.fn(),
}));
describe('BackgroundMigrationsDatabaseListbox', () => {
let wrapper;
const defaultProps = {
databases: MOCK_DATABASES,
selectedDatabase: MOCK_SELECTED_DATABASE,
};
const createComponent = (props = {}) => {
wrapper = shallowMount(BackgroundMigrationsDatabaseListbox, {
propsData: {
...defaultProps,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findGlListbox = () => wrapper.findComponent(GlListbox);
describe('template always', () => {
beforeEach(() => {
createComponent();
});
it('renders GlListbox', () => {
expect(findGlListbox().exists()).toBe(true);
});
});
describe('actions', () => {
beforeEach(() => {
createComponent();
});
it('selecting a listbox item fires visitUrl with the database param', () => {
findGlListbox().vm.$emit('select', MOCK_DATABASES[1].value);
expect(setUrlParams).toHaveBeenCalledWith({ database: MOCK_DATABASES[1].value });
expect(visitUrl).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,6 @@
export const MOCK_DATABASES = [
{ value: 'main', text: 'main' },
{ value: 'ci', text: 'ci' },
];
export const MOCK_SELECTED_DATABASE = 'main';

View file

@ -124,7 +124,7 @@ describe('content_editor/components/bubble_menus/code_block', () => {
describe('when dropdown item is clicked', () => {
beforeEach(async () => {
jest.spyOn(codeBlockLanguageLoader, 'loadLanguages').mockResolvedValue();
jest.spyOn(codeBlockLanguageLoader, 'loadLanguage').mockResolvedValue();
findDropdownItems().at(1).vm.$emit('click');
@ -132,7 +132,7 @@ describe('content_editor/components/bubble_menus/code_block', () => {
});
it('loads language', () => {
expect(codeBlockLanguageLoader.loadLanguages).toHaveBeenCalledWith(['java']);
expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith('java');
});
it('sets code block', () => {

View file

@ -1,20 +1,33 @@
import { nextTick } from 'vue';
import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
import { shallowMount } from '@vue/test-utils';
import FrontmatterWrapper from '~/content_editor/components/wrappers/frontmatter.vue';
import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vue';
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
describe('content/components/wrappers/frontmatter', () => {
jest.mock('~/content_editor/services/code_block_language_loader');
describe('content/components/wrappers/code_block', () => {
const language = 'yaml';
let wrapper;
let updateAttributesFn;
const createWrapper = async (nodeAttrs = { language: 'yaml' }) => {
wrapper = shallowMount(FrontmatterWrapper, {
const createWrapper = async (nodeAttrs = { language }) => {
updateAttributesFn = jest.fn();
wrapper = shallowMount(CodeBlockWrapper, {
propsData: {
node: {
attrs: nodeAttrs,
},
updateAttributes: updateAttributesFn,
},
});
};
beforeEach(() => {
codeBlockLanguageLoader.findLanguageBySyntax.mockReturnValue({ syntax: language });
});
afterEach(() => {
wrapper.destroy();
});
@ -38,11 +51,21 @@ describe('content/components/wrappers/frontmatter', () => {
});
it('renders label indicating that code block is frontmatter', () => {
createWrapper();
createWrapper({ isFrontmatter: true, language });
const label = wrapper.find('[data-testid="frontmatter-label"]');
expect(label.text()).toEqual('frontmatter:yaml');
expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']);
});
it('loads code blocks syntax highlight language', async () => {
createWrapper();
expect(codeBlockLanguageLoader.loadLanguage).toHaveBeenCalledWith(language);
await nextTick();
expect(updateAttributesFn).toHaveBeenCalledWith({ language });
});
});

View file

@ -1,4 +1,5 @@
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import languageLoader from '~/content_editor/services/code_block_language_loader';
import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true">
@ -9,20 +10,20 @@ const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language
</code>
</pre>`;
jest.mock('~/content_editor/services/code_block_language_loader');
describe('content_editor/extensions/code_block_highlight', () => {
let parsedCodeBlockHtmlFixture;
let tiptapEditor;
let doc;
let codeBlock;
let languageLoader;
const parseHTML = (html) => new DOMParser().parseFromString(html, 'text/html');
const preElement = () => parsedCodeBlockHtmlFixture.querySelector('pre');
beforeEach(() => {
languageLoader = { loadLanguages: jest.fn() };
tiptapEditor = createTestEditor({
extensions: [CodeBlockHighlight.configure({ languageLoader })],
extensions: [CodeBlockHighlight],
});
({
@ -70,6 +71,8 @@ describe('content_editor/extensions/code_block_highlight', () => {
const language = 'javascript';
beforeEach(() => {
languageLoader.loadLanguageFromInputRule.mockReturnValueOnce({ language });
triggerNodeInputRule({
tiptapEditor,
inputRuleText: `${inputRule}${language} `,
@ -83,7 +86,9 @@ describe('content_editor/extensions/code_block_highlight', () => {
});
it('loads language when language loader is available', () => {
expect(languageLoader.loadLanguages).toHaveBeenCalledWith([language]);
expect(languageLoader.loadLanguageFromInputRule).toHaveBeenCalledWith(
expect.arrayContaining([`${inputRule}${language} `, language]),
);
});
});
});

View file

@ -0,0 +1,16 @@
import Diagram from '~/content_editor/extensions/diagram';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
describe('content_editor/extensions/diagram', () => {
it('inherits from code block highlight extension', () => {
expect(Diagram.parent).toBe(CodeBlockHighlight);
});
it('sets isDiagram attribute to true by default', () => {
expect(Diagram.config.addAttributes()).toEqual(
expect.objectContaining({
isDiagram: { default: true },
}),
);
});
});

View file

@ -22,6 +22,10 @@ describe('content_editor/extensions/frontmatter', () => {
}));
});
it('inherits from code block highlight extension', () => {
expect(Frontmatter.parent).toBe(CodeBlockHighlight);
});
it('does not insert a frontmatter block when executing code block input rule', () => {
const expectedDoc = doc(codeBlock({ language: 'plaintext' }, ''));
const inputRuleText = '``` ';
@ -31,6 +35,14 @@ describe('content_editor/extensions/frontmatter', () => {
expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
});
it('sets isFrontmatter attribute to true by default', () => {
expect(Frontmatter.config.addAttributes()).toEqual(
expect.objectContaining({
isFrontmatter: { default: true },
}),
);
});
it.each`
command | result | resultDesc
${'toggleCodeBlock'} | ${() => doc(codeBlock(''))} | ${'code block element'}

View file

@ -53,23 +53,19 @@ describe('content_editor/services/code_block_language_loader', () => {
});
});
describe('loadLanguages', () => {
describe('loadLanguage', () => {
it('loads highlight.js language packages identified by a list of languages', async () => {
const languages = ['javascript', 'ruby'];
const language = 'javascript';
await languageLoader.loadLanguages(languages);
await languageLoader.loadLanguage(language);
languages.forEach((language) => {
expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function));
});
expect(lowlight.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function));
});
describe('when language is already registered', () => {
it('does not load the language again', async () => {
const languages = ['javascript'];
await languageLoader.loadLanguages(languages);
await languageLoader.loadLanguages(languages);
await languageLoader.loadLanguage('javascript');
await languageLoader.loadLanguage('javascript');
expect(lowlight.registerLanguage).toHaveBeenCalledTimes(1);
});
@ -94,7 +90,7 @@ describe('content_editor/services/code_block_language_loader', () => {
expect(languageLoader.isLanguageLoaded(language)).toBe(false);
await languageLoader.loadLanguages([language]);
await languageLoader.loadLanguage(language);
expect(languageLoader.isLanguageLoaded(language)).toBe(true);
});

View file

@ -11,7 +11,6 @@ describe('content_editor/services/content_editor', () => {
let contentEditor;
let serializer;
let deserializer;
let languageLoader;
let eventHub;
let doc;
let p;
@ -28,14 +27,12 @@ describe('content_editor/services/content_editor', () => {
serializer = { serialize: jest.fn() };
deserializer = { deserialize: jest.fn() };
languageLoader = { loadLanguages: jest.fn() };
eventHub = eventHubFactory();
contentEditor = new ContentEditor({
tiptapEditor,
serializer,
deserializer,
eventHub,
languageLoader,
});
});
@ -77,12 +74,6 @@ describe('content_editor/services/content_editor', () => {
expect(contentEditor.tiptapEditor.state.doc.toJSON()).toEqual(document.toJSON());
});
it('passes deserialized DOM document to language loader', async () => {
await contentEditor.setSerializedContent(testMarkdown);
expect(languageLoader.loadLanguages).toHaveBeenCalledWith(languages);
});
});
describe('when setSerializedContent fails', () => {

View file

@ -46,10 +46,6 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
expect(result.document.toJSON()).toEqual(document.toJSON());
});
it('returns languages of code blocks found in the document', () => {
expect(result.languages).toEqual(['javascript']);
});
});
describe('when the render function returns an empty value', () => {

View file

@ -273,7 +273,7 @@ two
markdown: `
const fn = () => 'GitLab';
`,
doc: doc(codeBlock({ language: '' }, "const fn = () => 'GitLab';\n")),
doc: doc(codeBlock({ language: null }, "const fn = () => 'GitLab';")),
},
{
markdown: `
@ -281,14 +281,14 @@ two
const fn = () => 'GitLab';
\`\`\`\
`,
doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';\n")),
doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';")),
},
{
markdown: `
\`\`\`
\`\`\`\
`,
doc: doc(codeBlock({ language: '' }, '')),
doc: doc(codeBlock({ language: null }, '')),
},
{
markdown: `
@ -298,7 +298,7 @@ two
\`\`\`\
`,
doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';\n\n\n")),
doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';\n\n")),
},
])('deserializes %s correctly', async ({ markdown, doc: expectedDoc }) => {
const { schema } = tiptapEditor;

View file

@ -97,7 +97,10 @@
"expire_in": "1 week",
"reports": {
"junit": "result.xml",
"cobertura": "cobertura-coverage.xml",
"coverage_report": {
"coverage_format": "cobertura",
"path": "cobertura-coverage.xml"
},
"codequality": "codequality.json",
"sast": "sast.json",
"dependency_scanning": "scan.json",
@ -147,7 +150,10 @@
"artifacts": {
"reports": {
"junit": ["result.xml"],
"cobertura": ["cobertura-coverage.xml"],
"coverage_report": {
"coverage_format": "cobertura",
"path": "cobertura-coverage.xml"
},
"codequality": ["codequality.json"],
"sast": ["sast.json"],
"dependency_scanning": ["scan.json"],

View file

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepositoryDetails'] do
fields = %i[id name path location created_at updated_at expiration_policy_started_at
status tags_count can_delete expiration_policy_cleanup_status tags size
project migration_state]
project migration_state last_cleanup_deleted_tags_count]
it { expect(described_class.graphql_name).to eq('ContainerRepositoryDetails') }

View file

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['ContainerRepository'] do
fields = %i[id name path location created_at updated_at expiration_policy_started_at
status tags_count can_delete expiration_policy_cleanup_status project
migration_state]
migration_state last_cleanup_deleted_tags_count]
it { expect(described_class.graphql_name).to eq('ContainerRepository') }

View file

@ -45,7 +45,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do
:load_performance | 'load-performance.json'
:lsif | 'lsif.json'
:dotenv | 'build.dotenv'
:cobertura | 'cobertura-coverage.xml'
:terraform | 'tfplan.json'
:accessibility | 'gl-accessibility.json'
end
@ -89,18 +88,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports do
expect(entry.value).to eq({ coverage_report: coverage_report, dast: ['gl-dast-report.json'] })
end
end
context 'and a direct coverage report format is specified' do
let(:config) { { coverage_report: coverage_report, cobertura: 'cobertura-coverage.xml' } }
it 'is not valid' do
expect(entry).not_to be_valid
end
it 'reports error' do
expect(entry.errors).to include /please use only one the following keys: coverage_report, cobertura/
end
end
end
end

View file

@ -3,6 +3,31 @@
require 'spec_helper'
RSpec.describe RawUsageData do
context 'scopes' do
describe '.for_current_reporting_cycle' do
subject(:recent_service_ping_reports) { described_class.for_current_reporting_cycle }
before_all do
create(:raw_usage_data, created_at: (described_class::REPORTING_CADENCE + 1.day).ago)
end
it 'returns nil where no records match filter criteria' do
expect(recent_service_ping_reports).to be_empty
end
context 'with records matching filtering criteria' do
let_it_be(:fresh_record) { create(:raw_usage_data) }
let_it_be(:record_at_edge_of_time_range) do
create(:raw_usage_data, created_at: described_class::REPORTING_CADENCE.ago)
end
it 'return records within reporting cycle time range ordered by creation time' do
expect(recent_service_ping_reports).to eq [fresh_record, record_at_edge_of_time_range]
end
end
end
end
describe 'validations' do
it { is_expected.to validate_presence_of(:payload) }
it { is_expected.to validate_presence_of(:recorded_at) }