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 }}
- - + + @@ -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 @@ + + 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 @@ + + + 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}")