diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 2ae81dbb5ba..8a4ea690c60 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -429,6 +429,13 @@ db:check-migrations-decomposed:
- .decomposed-database
- .rails:rules:decomposed-databases
+db:migrate-non-superuser:
+ extends:
+ - .db-job-base
+ - .rails:rules:ee-and-foss-mr-with-migration
+ script:
+ - bundle exec rake gitlab:db:reset_as_non_superuser
+
db:gitlabcom-database-testing:
extends: .rails:rules:db:gitlabcom-database-testing
stage: test
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index 2f1dbfce860..8204731e67a 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -230,6 +230,9 @@
.controllers-patterns: &controllers-patterns
- "{,ee/,jh/}{app/controllers}/**/*"
+.models-patterns: &models-patterns
+ - "{,ee/,jh/}{app/models}/**/*"
+
.startup-css-patterns: &startup-css-patterns
- "{,ee/,jh/}app/assets/stylesheets/startup/**/*"
@@ -1429,6 +1432,8 @@
changes: *frontend-patterns
- <<: *if-dot-com-gitlab-org-merge-request
changes: *controllers-patterns
+ - <<: *if-dot-com-gitlab-org-merge-request
+ changes: *models-patterns
- <<: *if-dot-com-gitlab-org-merge-request
changes: *qa-patterns
- <<: *if-dot-com-gitlab-org-merge-request
diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue
index 8c5040035d4..f98edb6bb7d 100644
--- a/app/assets/javascripts/environments/components/deployment.vue
+++ b/app/assets/javascripts/environments/components/deployment.vue
@@ -6,10 +6,10 @@ import {
GlIcon,
GlLink,
GlTooltipDirective as GlTooltip,
+ GlTruncate,
} from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { __, s__ } from '~/locale';
-import { truncate } from '~/lib/utils/text_utility';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeploymentStatusBadge from './deployment_status_badge.vue';
@@ -25,6 +25,7 @@ export default {
GlCollapse,
GlIcon,
GlLink,
+ GlTruncate,
TimeAgoTooltip,
},
directives: {
@@ -75,7 +76,7 @@ export default {
return this.deployment?.user;
},
username() {
- return truncate(this.user?.username, 25);
+ return `@${this.user.username}`;
},
userPath() {
return this.user?.path;
@@ -84,11 +85,23 @@ export default {
return this.deployment?.deployable;
},
jobName() {
- return truncate(this.deployable?.name ?? '', 25);
+ return this.deployable?.name;
},
jobPath() {
return this.deployable?.buildPath;
},
+ refLabel() {
+ return this.deployment?.tag ? this.$options.i18n.tag : this.$options.i18n.branch;
+ },
+ ref() {
+ return this.deployment?.ref;
+ },
+ refName() {
+ return this.ref?.name;
+ },
+ refPath() {
+ return this.ref?.refPath;
+ },
},
methods: {
toggleCollapse() {
@@ -105,6 +118,8 @@ export default {
triggerer: s__('Deployment|Triggerer'),
job: __('Job'),
api: __('API'),
+ branch: __('Branch'),
+ tag: __('Tag'),
},
headerClasses: [
'gl-display-flex',
@@ -144,10 +159,12 @@ export default {
- #{{ iid }}
+
+ #{{ iid }}
-
- {{ timeAgo }}
+
+
+
+ {{ timeAgo }}
+
@@ -180,25 +200,40 @@ export default {
-
-
-
{{ $options.i18n.triggerer }}
-
@{{ username }}
+
+
+ {{ $options.i18n.triggerer }}
+
+
+
-
-
+
+
{{ $options.i18n.job }}
- {{ jobName }}
+
- {{ jobName }}
+
{{ $options.i18n.api }}
+
+ {{ refLabel }}
+
+
+
+
diff --git a/app/assets/javascripts/runner/components/cells/link_cell.vue b/app/assets/javascripts/runner/components/cells/link_cell.vue
new file mode 100644
index 00000000000..2843ddbacaf
--- /dev/null
+++ b/app/assets/javascripts/runner/components/cells/link_cell.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/runner/components/runner_details.vue b/app/assets/javascripts/runner/components/runner_details.vue
index ab4b99d4186..b6a5ffc7a64 100644
--- a/app/assets/javascripts/runner/components/runner_details.vue
+++ b/app/assets/javascripts/runner/components/runner_details.vue
@@ -1,22 +1,26 @@
+
+
+
+
+
+
{{ $options.I18N_NO_JOBS_FOUND }}
+
+
+
+
diff --git a/app/assets/javascripts/runner/components/runner_jobs_table.vue b/app/assets/javascripts/runner/components/runner_jobs_table.vue
new file mode 100644
index 00000000000..7817577bab0
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_jobs_table.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+ #{{ jobId(item) }}
+
+
+
+ {{ projectName(item) }}
+
+
+
+ {{ commitShortSha(item) }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index 45e61768d1e..1544efaaae2 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -4,6 +4,7 @@ export const RUNNER_PAGE_SIZE = 20;
export const RUNNER_JOB_COUNT_LIMIT = 1000;
export const RUNNER_DETAILS_PROJECTS_PAGE_SIZE = 5;
+export const RUNNER_DETAILS_JOBS_PAGE_SIZE = 30;
export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
@@ -45,6 +46,7 @@ export const I18N_PAUSED_RUNNER_DESCRIPTION = s__('Runners|Not available to run
export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})');
export const I18N_NONE = __('None');
+export const I18N_NO_JOBS_FOUND = s__('Runner|This runner has not run any jobs.');
// Styles
diff --git a/app/assets/javascripts/runner/graphql/get_runner_jobs.query.graphql b/app/assets/javascripts/runner/graphql/get_runner_jobs.query.graphql
new file mode 100644
index 00000000000..2b1decd3ddd
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/get_runner_jobs.query.graphql
@@ -0,0 +1,36 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getRunnerJobs($id: CiRunnerID!, $first: Int, $last: Int, $before: String, $after: String) {
+ runner(id: $id) {
+ id
+ projectCount
+ jobs(before: $before, after: $after, first: $first, last: $last) {
+ nodes {
+ id
+ detailedStatus {
+ # fields for `
`
+ id
+ detailsPath
+ group
+ icon
+ text
+ }
+ pipeline {
+ id
+ project {
+ id
+ name
+ webUrl
+ }
+ }
+ shortSha
+ commitPath
+ tags
+ finishedAt
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
index ae29fa3a4df..74760bbaa07 100644
--- a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
@@ -8,6 +8,7 @@ fragment RunnerDetailsShared on CiRunner {
ipAddress
description
maximumTimeout
+ jobCount
tagList
createdAt
status(legacyMode: null)
diff --git a/config/gitlab_loose_foreign_keys.yml b/config/gitlab_loose_foreign_keys.yml
index 154e563ad99..f46e005dacf 100644
--- a/config/gitlab_loose_foreign_keys.yml
+++ b/config/gitlab_loose_foreign_keys.yml
@@ -1,37 +1,36 @@
+# Make sure that this file has the keys sorted
---
-dast_site_profiles_pipelines:
- - table: ci_pipelines
- column: ci_pipeline_id
- on_delete: async_delete
-vulnerability_feedback:
- - table: ci_pipelines
- column: pipeline_id
- on_delete: async_nullify
-ci_pipeline_chat_data:
- - table: chat_names
- column: chat_name_id
- on_delete: async_delete
-dast_scanner_profiles_builds:
- - table: ci_builds
- column: ci_build_id
- on_delete: async_delete
-dast_site_profiles_builds:
- - table: ci_builds
- column: ci_build_id
- on_delete: async_delete
-dast_profiles_pipelines:
- - table: ci_pipelines
- column: ci_pipeline_id
- on_delete: async_delete
-clusters_applications_runners:
- - table: ci_runners
- column: runner_id
- on_delete: async_nullify
-ci_variables:
+ci_build_report_results:
- table: projects
column: project_id
on_delete: async_delete
-ci_runner_projects:
+ci_builds:
+ - table: users
+ column: user_id
+ on_delete: async_nullify
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_builds_metadata:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_daily_build_group_report_results:
+ - table: namespaces
+ column: group_id
+ on_delete: async_delete
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_freeze_periods:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_group_variables:
+ - table: namespaces
+ column: group_id
+ on_delete: async_delete
+ci_job_artifacts:
- table: projects
column: project_id
on_delete: async_delete
@@ -45,20 +44,13 @@ ci_job_token_project_scope_links:
- table: projects
column: target_project_id
on_delete: async_delete
-ci_daily_build_group_report_results:
+ci_minutes_additional_packs:
- table: namespaces
- column: group_id
+ column: namespace_id
on_delete: async_delete
- - table: projects
- column: project_id
- on_delete: async_delete
-external_pull_requests:
- - table: projects
- column: project_id
- on_delete: async_delete
-ci_freeze_periods:
- - table: projects
- column: project_id
+ci_namespace_mirrors:
+ - table: namespaces
+ column: namespace_id
on_delete: async_delete
ci_pending_builds:
- table: namespaces
@@ -67,37 +59,17 @@ ci_pending_builds:
- table: projects
column: project_id
on_delete: async_delete
-ci_resource_groups:
+ci_pipeline_artifacts:
- table: projects
column: project_id
on_delete: async_delete
-ci_runner_namespaces:
- - table: namespaces
- column: namespace_id
+ci_pipeline_chat_data:
+ - table: chat_names
+ column: chat_name_id
on_delete: async_delete
-ci_running_builds:
- - table: projects
- column: project_id
- on_delete: async_delete
-ci_namespace_mirrors:
- - table: namespaces
- column: namespace_id
- on_delete: async_delete
-ci_sources_projects:
- - table: projects
- column: source_project_id
- on_delete: async_delete
-ci_build_report_results:
- - table: projects
- column: project_id
- on_delete: async_delete
-ci_job_artifacts:
- - table: projects
- column: project_id
- on_delete: async_delete
-ci_builds:
+ci_pipeline_schedules:
- table: users
- column: user_id
+ column: owner_id
on_delete: async_nullify
- table: projects
column: project_id
@@ -122,97 +94,31 @@ ci_project_mirrors:
- table: namespaces
column: namespace_id
on_delete: async_delete
-ci_unit_tests:
- - table: projects
- column: project_id
- on_delete: async_delete
-merge_requests:
- - table: ci_pipelines
- column: head_pipeline_id
- on_delete: async_nullify
-vulnerability_statistics:
- - table: ci_pipelines
- column: latest_pipeline_id
- on_delete: async_nullify
-vulnerability_occurrence_pipelines:
- - table: ci_pipelines
- column: pipeline_id
- on_delete: async_delete
-packages_build_infos:
- - table: ci_pipelines
- column: pipeline_id
- on_delete: async_nullify
-packages_package_file_build_infos:
- - table: ci_pipelines
- column: pipeline_id
- on_delete: async_nullify
ci_project_monthly_usages:
- table: projects
column: project_id
on_delete: async_delete
-pages_deployments:
- - table: ci_builds
- column: ci_build_id
- on_delete: async_nullify
-ci_builds_metadata:
- - table: projects
- column: project_id
- on_delete: async_delete
-terraform_state_versions:
- - table: ci_builds
- column: ci_build_id
- on_delete: async_nullify
-merge_request_metrics:
- - table: ci_pipelines
- column: pipeline_id
- on_delete: async_delete
-project_pages_metadata:
- - table: ci_job_artifacts
- column: artifacts_archive_id
- on_delete: async_nullify
-ci_pipeline_schedules:
- - table: users
- column: owner_id
- on_delete: async_nullify
- - table: projects
- column: project_id
- on_delete: async_delete
-merge_trains:
- - table: ci_pipelines
- column: pipeline_id
- on_delete: async_nullify
ci_refs:
- table: projects
column: project_id
on_delete: async_delete
-ci_group_variables:
- - table: namespaces
- column: group_id
- on_delete: async_delete
-ci_minutes_additional_packs:
- - table: namespaces
- column: namespace_id
- on_delete: async_delete
-requirements_management_test_reports:
- - table: ci_builds
- column: build_id
- on_delete: async_nullify
-ci_subscriptions_projects:
- - table: projects
- column: downstream_project_id
- on_delete: async_delete
- - table: projects
- column: upstream_project_id
- on_delete: async_delete
-security_scans:
- - table: ci_builds
- column: build_id
- on_delete: async_delete
-ci_secure_files:
+ci_resource_groups:
- table: projects
column: project_id
on_delete: async_delete
-ci_pipeline_artifacts:
+ci_runner_namespaces:
+ - table: namespaces
+ column: namespace_id
+ on_delete: async_delete
+ci_runner_projects:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_running_builds:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_secure_files:
- table: projects
column: project_id
on_delete: async_delete
@@ -223,10 +129,21 @@ ci_sources_pipelines:
- table: projects
column: project_id
on_delete: async_delete
+ci_sources_projects:
+ - table: projects
+ column: source_project_id
+ on_delete: async_delete
ci_stages:
- table: projects
column: project_id
on_delete: async_delete
+ci_subscriptions_projects:
+ - table: projects
+ column: downstream_project_id
+ on_delete: async_delete
+ - table: projects
+ column: upstream_project_id
+ on_delete: async_delete
ci_triggers:
- table: users
column: owner_id
@@ -234,3 +151,87 @@ ci_triggers:
- table: projects
column: project_id
on_delete: async_delete
+ci_unit_tests:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+ci_variables:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+clusters_applications_runners:
+ - table: ci_runners
+ column: runner_id
+ on_delete: async_nullify
+dast_profiles_pipelines:
+ - table: ci_pipelines
+ column: ci_pipeline_id
+ on_delete: async_delete
+dast_scanner_profiles_builds:
+ - table: ci_builds
+ column: ci_build_id
+ on_delete: async_delete
+dast_site_profiles_builds:
+ - table: ci_builds
+ column: ci_build_id
+ on_delete: async_delete
+dast_site_profiles_pipelines:
+ - table: ci_pipelines
+ column: ci_pipeline_id
+ on_delete: async_delete
+external_pull_requests:
+ - table: projects
+ column: project_id
+ on_delete: async_delete
+merge_request_metrics:
+ - table: ci_pipelines
+ column: pipeline_id
+ on_delete: async_delete
+merge_requests:
+ - table: ci_pipelines
+ column: head_pipeline_id
+ on_delete: async_nullify
+merge_trains:
+ - table: ci_pipelines
+ column: pipeline_id
+ on_delete: async_nullify
+packages_build_infos:
+ - table: ci_pipelines
+ column: pipeline_id
+ on_delete: async_nullify
+packages_package_file_build_infos:
+ - table: ci_pipelines
+ column: pipeline_id
+ on_delete: async_nullify
+pages_deployments:
+ - table: ci_builds
+ column: ci_build_id
+ on_delete: async_nullify
+project_pages_metadata:
+ - table: ci_job_artifacts
+ column: artifacts_archive_id
+ on_delete: async_nullify
+requirements_management_test_reports:
+ - table: ci_builds
+ column: build_id
+ on_delete: async_nullify
+security_scans:
+ - table: ci_builds
+ column: build_id
+ on_delete: async_delete
+terraform_state_versions:
+ - table: ci_builds
+ column: ci_build_id
+ on_delete: async_nullify
+vulnerability_feedback:
+ - table: ci_pipelines
+ column: pipeline_id
+ on_delete: async_nullify
+vulnerability_occurrence_pipelines:
+ - table: ci_pipelines
+ column: pipeline_id
+ on_delete: async_delete
+vulnerability_statistics:
+ - table: ci_pipelines
+ column: latest_pipeline_id
+ on_delete: async_nullify
diff --git a/db/migrate/20220119220620_add_scan_method_to_dast_site_profile.rb b/db/migrate/20220119220620_add_scan_method_to_dast_site_profile.rb
new file mode 100644
index 00000000000..f7b7580d673
--- /dev/null
+++ b/db/migrate/20220119220620_add_scan_method_to_dast_site_profile.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddScanMethodToDastSiteProfile < Gitlab::Database::Migration[1.0]
+ def up
+ add_column :dast_site_profiles, :scan_method, :integer, limit: 2, default: 0, null: false
+ end
+
+ def down
+ remove_column :dast_site_profiles, :scan_method
+ end
+end
diff --git a/db/migrate/20220208171826_update_default_scan_method_of_dast_site_profile.rb b/db/migrate/20220208171826_update_default_scan_method_of_dast_site_profile.rb
new file mode 100644
index 00000000000..b01dbe642e2
--- /dev/null
+++ b/db/migrate/20220208171826_update_default_scan_method_of_dast_site_profile.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class UpdateDefaultScanMethodOfDastSiteProfile < Gitlab::Database::Migration[1.0]
+ BATCH_SIZE = 500
+
+ disable_ddl_transaction!
+
+ def up
+ each_batch_range('dast_site_profiles', scope: ->(table) { table.where(target_type: 1) }, of: BATCH_SIZE) do |min, max|
+ execute <<~SQL
+ UPDATE dast_site_profiles
+ SET scan_method = 1
+ WHERE id BETWEEN #{min} AND #{max}
+ SQL
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb b/db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb
new file mode 100644
index 00000000000..eccfab25126
--- /dev/null
+++ b/db/post_migrate/20220128155814_fix_approval_rules_code_owners_rule_type_index.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class FixApprovalRulesCodeOwnersRuleTypeIndex < Gitlab::Database::Migration[1.0]
+ INDEX_NAME = 'index_approval_rules_code_owners_rule_type'
+ OLD_INDEX_NAME = 'index_approval_rules_code_owners_rule_type_old'
+ TABLE = :approval_merge_request_rules
+ COLUMN = :merge_request_id
+ WHERE_CONDITION = 'rule_type = 2'
+
+ disable_ddl_transaction!
+
+ def up
+ rename_index TABLE, INDEX_NAME, OLD_INDEX_NAME if index_exists_by_name?(TABLE, INDEX_NAME) && !index_exists_by_name?(TABLE, OLD_INDEX_NAME)
+
+ add_concurrent_index TABLE, COLUMN, where: WHERE_CONDITION, name: INDEX_NAME
+
+ remove_concurrent_index_by_name TABLE, OLD_INDEX_NAME
+ end
+
+ def down
+ # No-op
+ end
+end
diff --git a/db/schema_migrations/20220119220620 b/db/schema_migrations/20220119220620
new file mode 100644
index 00000000000..a6a9abb2acc
--- /dev/null
+++ b/db/schema_migrations/20220119220620
@@ -0,0 +1 @@
+535f476a358dcb3f3472f1e0ec1afef738f995197b5d1f4fcd61e58a9c9e8e75
\ No newline at end of file
diff --git a/db/schema_migrations/20220128155814 b/db/schema_migrations/20220128155814
new file mode 100644
index 00000000000..209b0874a84
--- /dev/null
+++ b/db/schema_migrations/20220128155814
@@ -0,0 +1 @@
+77cc8fc86f2c6a5ed017dde40dd4db796821a35e6ce4d8dcbe24b2cdaccbb5d9
\ No newline at end of file
diff --git a/db/schema_migrations/20220208171826 b/db/schema_migrations/20220208171826
new file mode 100644
index 00000000000..75ae0dcef61
--- /dev/null
+++ b/db/schema_migrations/20220208171826
@@ -0,0 +1 @@
+e48473172d7561fb7474e16e291e555843c0ec4543300b007f86cd4a5923db85
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 6460a376664..4cdfa3594bf 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -13414,6 +13414,7 @@ CREATE TABLE dast_site_profiles (
auth_password_field text,
auth_username text,
target_type smallint DEFAULT 0 NOT NULL,
+ scan_method smallint DEFAULT 0 NOT NULL,
CONSTRAINT check_5203110fee CHECK ((char_length(auth_username_field) <= 255)),
CONSTRAINT check_6cfab17b48 CHECK ((char_length(name) <= 255)),
CONSTRAINT check_c329dffdba CHECK ((char_length(auth_password_field) <= 255)),
diff --git a/doc/.vale/gitlab/SubstitutionSuggestions.yml b/doc/.vale/gitlab/SubstitutionSuggestions.yml
index e7c0cc04244..df132d89637 100644
--- a/doc/.vale/gitlab/SubstitutionSuggestions.yml
+++ b/doc/.vale/gitlab/SubstitutionSuggestions.yml
@@ -17,6 +17,7 @@ swap:
e-mail: '"email"'
GFM: '"GitLab Flavored Markdown"'
it is recommended: '"we recommend"'
+ navigate: go
OAuth2: '"OAuth 2.0"'
once that: '"after that"'
once the: '"after the"'
diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md
index 2dc040662b7..2c435cdc69d 100644
--- a/doc/development/documentation/styleguide/word_list.md
+++ b/doc/development/documentation/styleguide/word_list.md
@@ -561,6 +561,8 @@ Do not use **navigate**. Use **go** instead. For example:
- Go to this webpage.
- Open a terminal and go to the `runner` directory.
+([Vale](../testing.md#vale) rule: [`SubstitutionSuggestions.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SubstitutionSuggestions.yml))
+
## need to, should
Try to avoid **needs to**, because it's wordy. Avoid **should** when you can be more specific. If something is required, use **must**.
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index a674c08dc9e..27d5ae70ed7 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -15,6 +15,7 @@ For any of the following scenarios, the `start-review-app-pipeline` job would be
- for merge requests with CI config changes
- for merge requests with frontend changes
- for merge requests with changes to `{,ee/,jh/}{app/controllers}/**/*`
+- for merge requests with changes to `{,ee/,jh/}{app/models}/**/*`
- for merge requests with QA changes
- for scheduled pipelines
- the MR has the `pipeline:run-review-app` label set
diff --git a/doc/user/analytics/img/issues_created_per_month_v13_11.png b/doc/user/analytics/img/issues_created_per_month_v13_11.png
deleted file mode 100644
index da3340bfdc2..00000000000
Binary files a/doc/user/analytics/img/issues_created_per_month_v13_11.png and /dev/null differ
diff --git a/doc/user/analytics/img/issues_created_per_month_v14_8.png b/doc/user/analytics/img/issues_created_per_month_v14_8.png
new file mode 100644
index 00000000000..7dcfa73ea19
Binary files /dev/null and b/doc/user/analytics/img/issues_created_per_month_v14_8.png differ
diff --git a/doc/user/analytics/issue_analytics.md b/doc/user/analytics/issue_analytics.md
index 6aa2f594532..62fff443073 100644
--- a/doc/user/analytics/issue_analytics.md
+++ b/doc/user/analytics/issue_analytics.md
@@ -34,7 +34,7 @@ You can change the total number of months displayed by setting a URL parameter.
For example, `https://gitlab.com/groups/gitlab-org/-/issues_analytics?months_back=15`
shows a total of 15 months for the chart in the GitLab.org group.
-![Issues created per month](img/issues_created_per_month_v13_11.png)
+![Issues created per month](img/issues_created_per_month_v14_8.png)
## Drill into the information
diff --git a/doc/user/profile/img/personal_readme_setup_v14_5.png b/doc/user/profile/img/personal_readme_setup_v14_5.png
new file mode 100644
index 00000000000..92d8e0ec936
Binary files /dev/null and b/doc/user/profile/img/personal_readme_setup_v14_5.png differ
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index cb009a8ec67..f201e04183c 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -102,20 +102,36 @@ user profiles are only visible to signed-in users.
## Add details to your profile with a README
-### *Add personal README to profile*
-
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232157) in GitLab 14.5.
-If you want to add more information to your profile page, you can create a README file. When you populate the README file with information, it's included on your profile page.
+You can add more information to your profile page with a README file. When you populate
+the README file with information, it's included on your profile page.
-To add a README to your profile:
+### From a new project
-1. Create a new public project with the same project path as your GitLab username.
+To create a new project and add its README to your profile:
+
+1. On the top bar, select **Menu > Project**.
+1. Select **Create new project**.
+1. Select **Create blank project**.
+1. Enter the project details:
+ - In the **Project name** field, enter the name for your new project.
+ - In the **Project URL** field, select your GitLab username.
+ - In the **Project slug** field, enter your GitLab username.
+1. For **Visibility Level**, select **Public**.
+ ![Proper project path for an individual on the hosted product](img/personal_readme_setup_v14_5.png)
+1. For **Project Configuration**, ensure **Initialize repository with a README** is selected.
+1. Select **Create project**.
1. Create a README file inside this project. The file can be any valid [README or index file](../project/repository/index.md#readme-and-index-files).
1. Populate the README file with [Markdown](../markdown.md).
-To use an existing project, [update the path](../project/settings/index.md#renaming-a-repository) of the project to match
-your username.
+GitLab displays the contents of your README below your contribution graph.
+
+### From an existing project
+
+To add the README from an existing project to your profile,
+[update the path](../project/settings/index.md#renaming-a-repository) of the project
+to match your username.
## Add external accounts to your user profile page
diff --git a/lib/gitlab/ci/lint.rb b/lib/gitlab/ci/lint.rb
index c93dfe3f202..5591ed62436 100644
--- a/lib/gitlab/ci/lint.rb
+++ b/lib/gitlab/ci/lint.rb
@@ -18,6 +18,8 @@ module Gitlab
end
end
+ LOG_MAX_DURATION_THRESHOLD = 2.seconds
+
def initialize(project:, current_user:, sha: nil)
@project = project
@current_user = current_user
@@ -49,12 +51,9 @@ module Gitlab
end
def static_validation(content)
- result = Gitlab::Ci::YamlProcessor.new(
- content,
- project: @project,
- user: @current_user,
- sha: @sha
- ).execute
+ logger = build_logger
+
+ result = yaml_processor_result(content, logger)
Result.new(
jobs: static_validation_convert_to_jobs(result),
@@ -62,6 +61,17 @@ module Gitlab
errors: result.errors,
warnings: result.warnings.take(::Gitlab::Ci::Warnings::MAX_LIMIT) # rubocop: disable CodeReuse/ActiveRecord
)
+ ensure
+ logger.commit(pipeline: ::Ci::Pipeline.new, caller: self.class.name)
+ end
+
+ def yaml_processor_result(content, logger)
+ logger.instrument(:yaml_process) do
+ Gitlab::Ci::YamlProcessor.new(content, project: @project,
+ user: @current_user,
+ sha: @sha,
+ logger: logger).execute
+ end
end
def dry_run_convert_to_jobs(stages)
@@ -109,6 +119,17 @@ module Gitlab
jobs
end
+
+ def build_logger
+ Gitlab::Ci::Pipeline::Logger.new(project: @project) do |l|
+ l.log_when do |observations|
+ values = observations['yaml_process_duration_s']
+ next false if values.empty?
+
+ values.max >= LOG_MAX_DURATION_THRESHOLD
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/logger.rb b/lib/gitlab/ci/pipeline/logger.rb
index fbba12c11a9..10c0fe295f8 100644
--- a/lib/gitlab/ci/pipeline/logger.rb
+++ b/lib/gitlab/ci/pipeline/logger.rb
@@ -59,7 +59,7 @@ module Gitlab
attributes = {
class: self.class.name.to_s,
pipeline_creation_caller: caller,
- project_id: project.id,
+ project_id: project&.id, # project is not available when called from `/ci/lint`
pipeline_persisted: pipeline.persisted?,
pipeline_source: pipeline.source,
pipeline_creation_service_duration_s: age
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index f9c346a272f..9b32d285ec0 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -109,6 +109,26 @@ module Gitlab
name.to_s == CI_DATABASE_NAME
end
+ class PgUser < ApplicationRecord
+ self.table_name = 'pg_user'
+ self.primary_key = :usename
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def self.check_for_non_superuser
+ user = PgUser.find_by('usename = CURRENT_USER')
+ am_i_superuser = user.usesuper
+
+ Gitlab::AppLogger.info(
+ "Account details: User: \"#{user.usename}\", UseSuper: (#{am_i_superuser})"
+ )
+
+ raise 'Error: detected superuser' if am_i_superuser
+ rescue ActiveRecord::StatementInvalid
+ raise 'User CURRENT_USER not found'
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def self.check_postgres_version_and_print_warning
return if Gitlab::Runtime.rails_runner?
diff --git a/lib/tasks/gitlab/db.rake b/lib/tasks/gitlab/db.rake
index 64277650d42..6d4af9d166f 100644
--- a/lib/tasks/gitlab/db.rake
+++ b/lib/tasks/gitlab/db.rake
@@ -270,6 +270,19 @@ namespace :gitlab do
end
end
+ desc 'Run migration as gitlab non-superuser'
+ task :reset_as_non_superuser, [:username] => :environment do |_, args|
+ username = args.fetch(:username, 'gitlab')
+ puts "Migrate using username #{username}"
+ Rake::Task['db:drop'].invoke
+ Rake::Task['db:create'].invoke
+ ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
+ ActiveRecord::Base.establish_connection(db_config.configuration_hash.merge(username: username)) # rubocop: disable Database/EstablishConnection
+ Gitlab::Database.check_for_non_superuser
+ Rake::Task['db:migrate'].invoke
+ end
+ end
+
# Only for development environments,
# we execute pending data migrations inline for convenience.
Rake::Task['db:migrate'].enhance do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 76a86981cc8..c2495a600d1 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -20374,6 +20374,9 @@ msgstr ""
msgid "It looks like you have some draft commits in this branch."
msgstr ""
+msgid "It looks like you're attempting to activate your subscription. Use %{a_start}the Subscription page%{a_end} instead."
+msgstr ""
+
msgid "It may be several days before you see feature usage data."
msgstr ""
@@ -20878,6 +20881,9 @@ msgstr ""
msgid "Job|Erase job log and artifacts"
msgstr ""
+msgid "Job|Finished at"
+msgstr ""
+
msgid "Job|Job artifacts"
msgstr ""
@@ -20902,6 +20908,9 @@ msgstr ""
msgid "Job|Show complete raw"
msgstr ""
+msgid "Job|Status"
+msgstr ""
+
msgid "Job|The artifacts were removed"
msgstr ""
@@ -27138,9 +27147,6 @@ msgstr ""
msgid "Please enter a valid time interval"
msgstr ""
-msgid "Please enter or upload a valid license."
-msgstr ""
-
msgid "Please enter your current password."
msgstr ""
@@ -31321,6 +31327,9 @@ msgstr ""
msgid "Runners|Instance"
msgstr ""
+msgid "Runners|Jobs"
+msgstr ""
+
msgid "Runners|Last contact"
msgstr ""
@@ -31561,6 +31570,9 @@ msgstr ""
msgid "Runners|stale"
msgstr ""
+msgid "Runner|This runner has not run any jobs."
+msgstr ""
+
msgid "Running"
msgstr ""
@@ -36336,6 +36348,9 @@ msgstr ""
msgid "The latest pipeline for this merge request has failed."
msgstr ""
+msgid "The license key is invalid."
+msgstr ""
+
msgid "The license key is invalid. Make sure it is exactly as you received it from GitLab Inc."
msgstr ""
@@ -36351,6 +36366,9 @@ msgstr ""
msgid "The license was successfully uploaded and will be active from %{starts_at}. You can see the details below."
msgstr ""
+msgid "The license you uploaded is invalid. If the issue persists, contact support at %{link}."
+msgstr ""
+
msgid "The list creation wizard is already open"
msgstr ""
diff --git a/spec/frontend/environments/deployment_spec.js b/spec/frontend/environments/deployment_spec.js
index 8936eb8fd64..6cc363e000b 100644
--- a/spec/frontend/environments/deployment_spec.js
+++ b/spec/frontend/environments/deployment_spec.js
@@ -213,8 +213,28 @@ describe('~/environments/components/deployment.vue', () => {
expect(job.attributes('href')).toBe(deployment.deployable.buildPath);
const apiBadge = wrapper.findByText(__('API'));
expect(apiBadge.exists()).toBe(false);
+
+ const branchLabel = wrapper.findByText(__('Branch'));
+ expect(branchLabel.exists()).toBe(true);
+ const tagLabel = wrapper.findByText(__('Tag'));
+ expect(tagLabel.exists()).toBe(false);
+ const ref = wrapper.findByRole('link', { name: deployment.ref.name });
+ expect(ref.attributes('href')).toBe(deployment.ref.refPath);
});
});
+
+ describe('with tagged deployment', () => {
+ beforeEach(async () => {
+ wrapper = createWrapper({ propsData: { deployment: { ...deployment, tag: true } } });
+ await wrapper.findComponent({ ref: 'details-toggle' }).trigger('click');
+ });
+
+ it('shows tag instead of branch', () => {
+ const refLabel = wrapper.findByText(__('Tag'));
+ expect(refLabel.exists()).toBe(true);
+ });
+ });
+
describe('with API deployment', () => {
beforeEach(async () => {
wrapper = createWrapper({ propsData: { deployment: { ...deployment, deployable: null } } });
@@ -237,7 +257,7 @@ describe('~/environments/components/deployment.vue', () => {
});
it('shows a span instead of a link', () => {
- const job = wrapper.findByText(deployment.deployable.name);
+ const job = wrapper.findByTitle(deployment.deployable.name);
expect(job.attributes('href')).toBeUndefined();
});
});
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index befb4e23b22..cdb4c3fd8ba 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -17,6 +17,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
let_it_be(:group_runner) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner', ip_address: '127.0.0.1') }
let_it_be(:group_runner_2) { create(:ci_runner, :group, groups: [group], active: false, version: '2.0.0', revision: '456', description: 'Group runner 2', ip_address: '127.0.0.1') }
let_it_be(:project_runner) { create(:ci_runner, :project, projects: [project, project_2], active: false, version: '2.0.0', revision: '456', description: 'Project runner', ip_address: '127.0.0.1') }
+ let_it_be(:build) { create(:ci_build, runner: instance_runner) }
query_path = 'runner/graphql/'
fixtures_path = 'graphql/runner/'
@@ -104,6 +105,22 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
end
+
+ describe GraphQL::Query, type: :request do
+ get_runner_jobs_query_name = 'get_runner_jobs.query.graphql'
+
+ let_it_be(:query) do
+ get_graphql_query_as_string("#{query_path}#{get_runner_jobs_query_name}")
+ end
+
+ it "#{fixtures_path}#{get_runner_jobs_query_name}.json" do
+ post_graphql(query, current_user: admin, variables: {
+ id: instance_runner.to_global_id.to_s
+ })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
end
describe do
diff --git a/spec/frontend/runner/components/cells/link_cell_spec.js b/spec/frontend/runner/components/cells/link_cell_spec.js
new file mode 100644
index 00000000000..a59a0eaa5d8
--- /dev/null
+++ b/spec/frontend/runner/components/cells/link_cell_spec.js
@@ -0,0 +1,72 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import LinkCell from '~/runner/components/cells/link_cell.vue';
+
+describe('LinkCell', () => {
+ let wrapper;
+
+ const findGlLink = () => wrapper.find(GlLink);
+ const findSpan = () => wrapper.find('span');
+
+ const createComponent = ({ props = {}, ...options } = {}) => {
+ wrapper = shallowMountExtended(LinkCell, {
+ propsData: {
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ it('when an href is provided, renders a link', () => {
+ createComponent({ props: { href: '/url' } });
+ expect(findGlLink().exists()).toBe(true);
+ });
+
+ it('when an href is not provided, renders no link', () => {
+ createComponent();
+ expect(findGlLink().exists()).toBe(false);
+ });
+
+ describe.each`
+ href | findContent
+ ${null} | ${findSpan}
+ ${'/url'} | ${findGlLink}
+ `('When href is $href', ({ href, findContent }) => {
+ const content = 'My Text';
+ const attrs = { foo: 'bar' };
+ const listeners = {
+ click: jest.fn(),
+ };
+
+ beforeEach(() => {
+ createComponent({
+ props: { href },
+ slots: {
+ default: content,
+ },
+ attrs,
+ listeners,
+ });
+ });
+
+ afterAll(() => {
+ listeners.click.mockReset();
+ });
+
+ it('Renders content', () => {
+ expect(findContent().text()).toBe(content);
+ });
+
+ it('Passes attributes', () => {
+ expect(findContent().attributes()).toMatchObject(attrs);
+ });
+
+ it('Passes event listeners', () => {
+ expect(listeners.click).toHaveBeenCalledTimes(0);
+
+ findContent().vm.$emit('click');
+
+ expect(listeners.click).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_details_spec.js b/spec/frontend/runner/components/runner_details_spec.js
index dbc96a30750..6bf4a52a799 100644
--- a/spec/frontend/runner/components/runner_details_spec.js
+++ b/spec/frontend/runner/components/runner_details_spec.js
@@ -1,4 +1,4 @@
-import { GlSprintf, GlIntersperse } from '@gitlab/ui';
+import { GlSprintf, GlIntersperse, GlTab } from '@gitlab/ui';
import { createWrapper, ErrorWrapper } from '@vue/test-utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -8,6 +8,7 @@ import { ACCESS_LEVEL_REF_PROTECTED, ACCESS_LEVEL_NOT_PROTECTED } from '~/runner
import RunnerDetails from '~/runner/components/runner_details.vue';
import RunnerDetail from '~/runner/components/runner_detail.vue';
import RunnerGroups from '~/runner/components/runner_groups.vue';
+import RunnersJobs from '~/runner/components/runner_jobs.vue';
import RunnerTags from '~/runner/components/runner_tags.vue';
import RunnerTag from '~/runner/components/runner_tag.vue';
@@ -38,6 +39,8 @@ describe('RunnerDetails', () => {
};
const findDetailGroups = () => wrapper.findComponent(RunnerGroups);
+ const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
+ const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
const createComponent = ({ props = {}, mountFn = shallowMountExtended, stubs } = {}) => {
wrapper = mountFn(RunnerDetails, {
@@ -146,4 +149,41 @@ describe('RunnerDetails', () => {
});
});
});
+
+ describe('Jobs tab', () => {
+ const stubs = { GlTab };
+
+ it('without a runner, shows no jobs', () => {
+ createComponent({
+ props: { runner: null },
+ stubs,
+ });
+
+ expect(findJobCountBadge().exists()).toBe(false);
+ expect(findRunnersJobs().exists()).toBe(false);
+ });
+
+ it('without a job count, shows no jobs count', () => {
+ createComponent({
+ props: {
+ runner: { ...mockRunner, jobCount: undefined },
+ },
+ stubs,
+ });
+
+ expect(findJobCountBadge().exists()).toBe(false);
+ });
+
+ it('with a job count, shows jobs count', () => {
+ const runner = { ...mockRunner, jobCount: 3 };
+
+ createComponent({
+ props: { runner },
+ stubs,
+ });
+
+ expect(findJobCountBadge().text()).toBe('3');
+ expect(findRunnersJobs().props('runner')).toBe(runner);
+ });
+ });
});
diff --git a/spec/frontend/runner/components/runner_jobs_spec.js b/spec/frontend/runner/components/runner_jobs_spec.js
new file mode 100644
index 00000000000..97339056370
--- /dev/null
+++ b/spec/frontend/runner/components/runner_jobs_spec.js
@@ -0,0 +1,156 @@
+import { GlSkeletonLoading } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import RunnerJobs from '~/runner/components/runner_jobs.vue';
+import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
+import RunnerPagination from '~/runner/components/runner_pagination.vue';
+import { captureException } from '~/runner/sentry_utils';
+import { I18N_NO_JOBS_FOUND, RUNNER_DETAILS_JOBS_PAGE_SIZE } from '~/runner/constants';
+
+import getRunnerJobsQuery from '~/runner/graphql/get_runner_jobs.query.graphql';
+
+import { runnerData, runnerJobsData } from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/runner/sentry_utils');
+
+const mockRunner = runnerData.data.runner;
+const mockRunnerWithJobs = runnerJobsData.data.runner;
+const mockJobs = mockRunnerWithJobs.jobs.nodes;
+
+Vue.use(VueApollo);
+
+describe('RunnerJobs', () => {
+ let wrapper;
+ let mockRunnerJobsQuery;
+
+ const findGlSkeletonLoading = () => wrapper.findComponent(GlSkeletonLoading);
+ const findRunnerJobsTable = () => wrapper.findComponent(RunnerJobsTable);
+ const findRunnerPagination = () => wrapper.findComponent(RunnerPagination);
+
+ const createComponent = ({ mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(RunnerJobs, {
+ apolloProvider: createMockApollo([[getRunnerJobsQuery, mockRunnerJobsQuery]]),
+ propsData: {
+ runner: mockRunner,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mockRunnerJobsQuery = jest.fn();
+ });
+
+ afterEach(() => {
+ mockRunnerJobsQuery.mockReset();
+ wrapper.destroy();
+ });
+
+ it('Requests runner jobs', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(1);
+ expect(mockRunnerJobsQuery).toHaveBeenCalledWith({
+ id: mockRunner.id,
+ first: RUNNER_DETAILS_JOBS_PAGE_SIZE,
+ });
+ });
+
+ describe('When there are jobs assigned', () => {
+ beforeEach(async () => {
+ mockRunnerJobsQuery.mockResolvedValueOnce(runnerJobsData);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('Shows jobs', () => {
+ const jobs = findRunnerJobsTable().props('jobs');
+
+ expect(jobs).toHaveLength(mockJobs.length);
+ expect(jobs[0]).toMatchObject(mockJobs[0]);
+ });
+
+ describe('When "Next" page is clicked', () => {
+ beforeEach(async () => {
+ findRunnerPagination().vm.$emit('input', { page: 2, after: 'AFTER_CURSOR' });
+
+ await waitForPromises();
+ });
+
+ it('A new page is requested', () => {
+ expect(mockRunnerJobsQuery).toHaveBeenCalledTimes(2);
+ expect(mockRunnerJobsQuery).toHaveBeenLastCalledWith({
+ id: mockRunner.id,
+ first: RUNNER_DETAILS_JOBS_PAGE_SIZE,
+ after: 'AFTER_CURSOR',
+ });
+ });
+ });
+ });
+
+ describe('When loading', () => {
+ it('shows loading indicator and no other content', () => {
+ createComponent();
+
+ expect(findGlSkeletonLoading().exists()).toBe(true);
+ expect(findRunnerJobsTable().exists()).toBe(false);
+ expect(findRunnerPagination().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('When there are no jobs', () => {
+ beforeEach(async () => {
+ mockRunnerJobsQuery.mockResolvedValueOnce({
+ data: {
+ runner: {
+ id: mockRunner.id,
+ projectCount: 0,
+ jobs: {
+ nodes: [],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
+ },
+ },
+ },
+ });
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('Shows a "None" label', () => {
+ expect(wrapper.text()).toBe(I18N_NO_JOBS_FOUND);
+ });
+ });
+
+ describe('When an error occurs', () => {
+ beforeEach(async () => {
+ mockRunnerJobsQuery.mockRejectedValue(new Error('Error!'));
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('shows an error', () => {
+ expect(createAlert).toHaveBeenCalled();
+ });
+
+ it('reports an error', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ component: 'RunnerJobs',
+ error: expect.any(Error),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_jobs_table_spec.js b/spec/frontend/runner/components/runner_jobs_table_spec.js
new file mode 100644
index 00000000000..5f4905ad2a8
--- /dev/null
+++ b/spec/frontend/runner/components/runner_jobs_table_spec.js
@@ -0,0 +1,119 @@
+import { GlTableLite } from '@gitlab/ui';
+import {
+ extendedWrapper,
+ shallowMountExtended,
+ mountExtended,
+} from 'helpers/vue_test_utils_helper';
+import { __, s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import RunnerJobsTable from '~/runner/components/runner_jobs_table.vue';
+import { useFakeDate } from 'helpers/fake_date';
+import { runnerJobsData } from '../mock_data';
+
+const mockJobs = runnerJobsData.data.runner.jobs.nodes;
+
+describe('RunnerJobsTable', () => {
+ let wrapper;
+ const mockNow = '2021-01-15T12:00:00Z';
+ const mockOneHourAgo = '2021-01-15T11:00:00Z';
+
+ useFakeDate(mockNow);
+
+ const findTable = () => wrapper.findComponent(GlTableLite);
+ const findHeaders = () => wrapper.findAll('th');
+ const findRows = () => wrapper.findAll('[data-testid^="job-row-"]');
+ const findCell = ({ field }) =>
+ extendedWrapper(findRows().at(0).find(`[data-testid="td-${field}"]`));
+
+ const createComponent = ({ props = {} } = {}, mountFn = shallowMountExtended) => {
+ wrapper = mountFn(RunnerJobsTable, {
+ propsData: {
+ jobs: mockJobs,
+ ...props,
+ },
+ stubs: {
+ GlTableLite,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Sets job id as a row key', () => {
+ createComponent();
+
+ expect(findTable().attributes('primarykey')).toBe('id');
+ });
+
+ describe('Table data', () => {
+ beforeEach(() => {
+ createComponent({}, mountExtended);
+ });
+
+ it('Displays headers', () => {
+ const headerLabels = findHeaders().wrappers.map((w) => w.text());
+
+ expect(headerLabels).toEqual([
+ s__('Job|Status'),
+ __('Job'),
+ __('Project'),
+ __('Commit'),
+ s__('Job|Finished at'),
+ s__('Runners|Tags'),
+ ]);
+ });
+
+ it('Displays a list of jobs', () => {
+ expect(findRows()).toHaveLength(1);
+ });
+
+ it('Displays details of a job', () => {
+ const { id, detailedStatus, pipeline, shortSha, commitPath } = mockJobs[0];
+
+ expect(findCell({ field: 'status' }).text()).toMatchInterpolatedText(detailedStatus.text);
+
+ expect(findCell({ field: 'job' }).text()).toContain(`#${getIdFromGraphQLId(id)}`);
+ expect(findCell({ field: 'job' }).find('a').attributes('href')).toBe(
+ detailedStatus.detailsPath,
+ );
+
+ expect(findCell({ field: 'project' }).text()).toBe(pipeline.project.name);
+ expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(
+ pipeline.project.webUrl,
+ );
+
+ expect(findCell({ field: 'commit' }).text()).toBe(shortSha);
+ expect(findCell({ field: 'commit' }).find('a').attributes('href')).toBe(commitPath);
+ });
+ });
+
+ describe('Table data formatting', () => {
+ let mockJobsCopy;
+
+ beforeEach(() => {
+ mockJobsCopy = [
+ {
+ ...mockJobs[0],
+ },
+ ];
+ });
+
+ it('Formats finishedAt time', () => {
+ mockJobsCopy[0].finishedAt = mockOneHourAgo;
+
+ createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
+
+ expect(findCell({ field: 'finished_at' }).text()).toBe('1 hour ago');
+ });
+
+ it('Formats tags', () => {
+ mockJobsCopy[0].tags = ['tag-1', 'tag-2'];
+
+ createComponent({ props: { jobs: mockJobsCopy } }, mountExtended);
+
+ expect(findCell({ field: 'tags' }).text()).toMatchInterpolatedText('tag-1 tag-2');
+ });
+ });
+});
diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js
index 36120a4c7ed..8b76be396ef 100644
--- a/spec/frontend/runner/components/runner_update_form_spec.js
+++ b/spec/frontend/runner/components/runner_update_form_spec.js
@@ -123,6 +123,7 @@ describe('RunnerUpdateForm', () => {
// Some read-only fields are not submitted
const {
+ __typename,
ipAddress,
runnerType,
createdAt,
@@ -132,7 +133,7 @@ describe('RunnerUpdateForm', () => {
userPermissions,
version,
groups,
- __typename,
+ jobCount,
...submitted
} = mockRunner;
diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js
index 7260f0fbc9a..d80caa47752 100644
--- a/spec/frontend/runner/mock_data.js
+++ b/spec/frontend/runner/mock_data.js
@@ -7,6 +7,7 @@ import runnersDataPaginated from 'test_fixtures/graphql/runner/get_runners.query
import runnerData from 'test_fixtures/graphql/runner/get_runner.query.graphql.json';
import runnerWithGroupData from 'test_fixtures/graphql/runner/get_runner.query.graphql.with_group.json';
import runnerProjectsData from 'test_fixtures/graphql/runner/get_runner_projects.query.graphql.json';
+import runnerJobsData from 'test_fixtures/graphql/runner/get_runner_jobs.query.graphql.json';
// Group queries
import groupRunnersData from 'test_fixtures/graphql/runner/get_group_runners.query.graphql.json';
@@ -20,6 +21,7 @@ export {
runnerData,
runnerWithGroupData,
runnerProjectsData,
+ runnerJobsData,
groupRunnersData,
groupRunnersCountData,
groupRunnersDataPaginated,
diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb
index 652fe0c4f94..747ff13c840 100644
--- a/spec/lib/gitlab/ci/lint_spec.rb
+++ b/spec/lib/gitlab/ci/lint_spec.rb
@@ -322,4 +322,102 @@ RSpec.describe Gitlab::Ci::Lint do
end
end
end
+
+ context 'pipeline logger' do
+ let(:counters) do
+ {
+ 'count' => a_kind_of(Numeric),
+ 'avg' => a_kind_of(Numeric),
+ 'max' => a_kind_of(Numeric),
+ 'min' => a_kind_of(Numeric)
+ }
+ end
+
+ let(:loggable_data) do
+ {
+ 'class' => 'Gitlab::Ci::Pipeline::Logger',
+ 'config_build_context_duration_s' => counters,
+ 'config_build_variables_duration_s' => counters,
+ 'config_compose_duration_s' => counters,
+ 'config_expand_duration_s' => counters,
+ 'config_external_process_duration_s' => counters,
+ 'config_stages_inject_duration_s' => counters,
+ 'config_tags_resolve_duration_s' => counters,
+ 'config_yaml_extend_duration_s' => counters,
+ 'config_yaml_load_duration_s' => counters,
+ 'pipeline_creation_caller' => 'Gitlab::Ci::Lint',
+ 'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
+ 'pipeline_persisted' => false,
+ 'pipeline_source' => 'unknown',
+ 'project_id' => project&.id,
+ 'yaml_process_duration_s' => counters
+ }
+ end
+
+ let(:content) do
+ <<~YAML
+ build:
+ script: echo
+ YAML
+ end
+
+ subject(:validate) { lint.validate(content, dry_run: false) }
+
+ before do
+ project&.add_developer(user)
+ end
+
+ context 'when the duration is under the threshold' do
+ it 'does not create a log entry' do
+ expect(Gitlab::AppJsonLogger).not_to receive(:info)
+
+ validate
+ end
+ end
+
+ context 'when the durations exceeds the threshold' do
+ let(:timer) do
+ proc do
+ @timer = @timer.to_i + 30
+ end
+ end
+
+ before do
+ allow(Gitlab::Ci::Pipeline::Logger)
+ .to receive(:current_monotonic_time) { timer.call }
+ end
+
+ it 'creates a log entry' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(loggable_data)
+
+ validate
+ end
+
+ context 'when the feature flag is disabled' do
+ before do
+ stub_feature_flags(ci_pipeline_creation_logger: false)
+ end
+
+ it 'does not create a log entry' do
+ expect(Gitlab::AppJsonLogger).not_to receive(:info)
+
+ validate
+ end
+ end
+
+ context 'when project is not provided' do
+ let(:project) { nil }
+
+ let(:project_nil_loggable_data) do
+ loggable_data.except('project_id')
+ end
+
+ it 'creates a log entry without project_id' do
+ expect(Gitlab::AppJsonLogger).to receive(:info).with(project_nil_loggable_data)
+
+ validate
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/logger_spec.rb b/spec/lib/gitlab/ci/pipeline/logger_spec.rb
index 1092824eddd..f31361431f2 100644
--- a/spec/lib/gitlab/ci/pipeline/logger_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/logger_spec.rb
@@ -203,6 +203,35 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do
expect(commit).to be_truthy
end
end
+
+ context 'when project is not passed and pipeline is not persisted' do
+ let(:project) {}
+ let(:pipeline) { build(:ci_pipeline) }
+
+ let(:loggable_data) do
+ {
+ 'class' => described_class.name.to_s,
+ 'pipeline_persisted' => false,
+ 'pipeline_creation_service_duration_s' => a_kind_of(Numeric),
+ 'pipeline_creation_caller' => 'source',
+ 'pipeline_save_duration_s' => {
+ 'avg' => 60, 'count' => 1, 'max' => 60, 'min' => 60
+ },
+ 'pipeline_creation_duration_s' => {
+ 'avg' => 20, 'count' => 2, 'max' => 30, 'min' => 10
+ }
+ }
+ end
+
+ it 'logs to application.json' do
+ expect(Gitlab::AppJsonLogger)
+ .to receive(:info)
+ .with(a_hash_including(loggable_data))
+ .and_call_original
+
+ expect(commit).to be_truthy
+ end
+ end
end
context 'when the feature flag is disabled' do
diff --git a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
index 297bcc27bcd..ed11699e494 100644
--- a/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
+++ b/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
@@ -18,6 +18,15 @@ RSpec.describe Gitlab::Database::LooseForeignKeys do
))
end
+ context 'ensure keys are sorted' do
+ it 'does not have any keys that are out of order' do
+ parsed = YAML.parse_file(described_class.loose_foreign_keys_yaml_path)
+ mapping = parsed.children.first
+ table_names = mapping.children.select(&:scalar?).map(&:value)
+ expect(table_names).to eq(table_names.sort), "expected sorted table names in the YAML file"
+ end
+ end
+
context 'ensure no duplicates are found' do
it 'does not have duplicate tables defined' do
# since we use hash to detect duplicate hash keys we need to parse YAML document
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 5ec7c338a2a..b3b7c81e9e7 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -104,6 +104,34 @@ RSpec.describe Gitlab::Database do
end
end
+ describe '.check_for_non_superuser' do
+ subject { described_class.check_for_non_superuser }
+
+ let(:non_superuser) { Gitlab::Database::PgUser.new(usename: 'foo', usesuper: false ) }
+ let(:superuser) { Gitlab::Database::PgUser.new(usename: 'bar', usesuper: true) }
+
+ it 'prints user details if not superuser' do
+ allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(non_superuser)
+
+ expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"foo\", UseSuper: (false)")
+
+ subject
+ end
+
+ it 'raises an exception if superuser' do
+ allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(superuser)
+
+ expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"bar\", UseSuper: (true)")
+ expect { subject }.to raise_error('Error: detected superuser')
+ end
+
+ it 'catches exception if find_by fails' do
+ allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_raise(ActiveRecord::StatementInvalid)
+
+ expect { subject }.to raise_error('User CURRENT_USER not found')
+ end
+ end
+
describe '.check_postgres_version_and_print_warning' do
let(:reflect) { instance_spy(Gitlab::Database::Reflection) }
diff --git a/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb
new file mode 100644
index 00000000000..1558facdf96
--- /dev/null
+++ b/spec/migrations/20220128155814_fix_approval_rules_code_owners_rule_type_index_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!('fix_approval_rules_code_owners_rule_type_index')
+
+RSpec.describe FixApprovalRulesCodeOwnersRuleTypeIndex, :migration do
+ let(:table_name) { :approval_merge_request_rules }
+ let(:index_name) { 'index_approval_rules_code_owners_rule_type' }
+
+ it 'correctly migrates up and down' do
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
+ }
+
+ migration.after -> {
+ expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
+ }
+ end
+ end
+
+ context 'when the index already exists' do
+ before do
+ subject.add_concurrent_index table_name, :merge_request_id, where: 'rule_type = 2', name: index_name
+ end
+
+ it 'keeps the index' do
+ migrate!
+
+ expect(subject.index_exists_by_name?(table_name, index_name)).to be_truthy
+ end
+ end
+end
diff --git a/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb
new file mode 100644
index 00000000000..b73aa7016a1
--- /dev/null
+++ b/spec/migrations/update_default_scan_method_of_dast_site_profile_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe UpdateDefaultScanMethodOfDastSiteProfile do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:dast_sites) { table(:dast_sites) }
+ let(:dast_site_profiles) { table(:dast_site_profiles) }
+
+ before do
+ namespace = namespaces.create!(name: 'test', path: 'test')
+ project = projects.create!(id: 12, namespace_id: namespace.id, name: 'gitlab', path: 'gitlab')
+ dast_site = dast_sites.create!(id: 1, url: 'https://www.gitlab.com', project_id: project.id)
+
+ dast_site_profiles.create!(id: 1, project_id: project.id, dast_site_id: dast_site.id,
+ name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 0",
+ scan_method: 0, target_type: 0)
+
+ dast_site_profiles.create!(id: 2, project_id: project.id, dast_site_id: dast_site.id,
+ name: "#{FFaker::Product.product_name.truncate(192)} #{SecureRandom.hex(4)} - 1",
+ scan_method: 0, target_type: 1)
+ end
+
+ it 'updates the scan_method to 1 for profiles with target_type 1' do
+ migrate!
+
+ expect(dast_site_profiles.where(scan_method: 1).count).to eq 1
+ expect(dast_site_profiles.where(scan_method: 0).count).to eq 1
+ end
+end
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index 4e9aac01c08..c3fd8135ae0 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -446,6 +446,44 @@ RSpec.describe 'gitlab:db namespace rake task', :silence_stdout do
end
end
+ describe 'gitlab:db:reset_as_non_superuser' do
+ let(:connection_pool) { instance_double(ActiveRecord::ConnectionAdapters::ConnectionPool ) }
+ let(:connection) { instance_double(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) }
+ let(:configurations) { double(ActiveRecord::DatabaseConfigurations) }
+ let(:configuration) { instance_double(ActiveRecord::DatabaseConfigurations::HashConfig) }
+ let(:config_hash) { { username: 'foo' } }
+
+ it 'migrate as nonsuperuser check with default username' do
+ allow(Rake::Task['db:drop']).to receive(:invoke)
+ allow(Rake::Task['db:create']).to receive(:invoke)
+ allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
+ allow(configurations).to receive(:configs_for).and_return([configuration])
+ allow(configuration).to receive(:configuration_hash).and_return(config_hash)
+ allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool)
+
+ expect(config_hash).to receive(:merge).with({ username: 'gitlab' })
+ expect(Gitlab::Database).to receive(:check_for_non_superuser)
+ expect(Rake::Task['db:migrate']).to receive(:invoke)
+
+ run_rake_task('gitlab:db:reset_as_non_superuser')
+ end
+
+ it 'migrate as nonsuperuser check with specified username' do
+ allow(Rake::Task['db:drop']).to receive(:invoke)
+ allow(Rake::Task['db:create']).to receive(:invoke)
+ allow(ActiveRecord::Base).to receive(:configurations).and_return(configurations)
+ allow(configurations).to receive(:configs_for).and_return([configuration])
+ allow(configuration).to receive(:configuration_hash).and_return(config_hash)
+ allow(ActiveRecord::Base).to receive(:establish_connection).and_return(connection_pool)
+
+ expect(config_hash).to receive(:merge).with({ username: 'foo' })
+ expect(Gitlab::Database).to receive(:check_for_non_superuser)
+ expect(Rake::Task['db:migrate']).to receive(:invoke)
+
+ run_rake_task('gitlab:db:reset_as_non_superuser', '[foo]')
+ end
+ end
+
def run_rake_task(task_name, arguments = '')
Rake::Task[task_name].reenable
Rake.application.invoke_task("#{task_name}#{arguments}")