diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-31 18:12:39 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-31 18:12:39 +0300 |
commit | 19044caf6695065ff64e26355e830dbdc6719e54 (patch) | |
tree | 5b9bdf081ab28d873cee38a5036f143a300ba733 | |
parent | 3034c7e6aa99d21c3d9fa1df01f60fdd3f32d914 (diff) |
Add latest changes from gitlab-org/gitlab@master
38 files changed, 678 insertions, 276 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 8ba690533ca..117a78a7325 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -4,7 +4,7 @@ include: - local: .gitlab/ci/global.gitlab-ci.yml - local: .gitlab/ci/package-and-test/rules.gitlab-ci.yml - project: gitlab-org/quality/pipeline-common - ref: 1.2.0 + ref: 1.2.1 file: - /ci/base.gitlab-ci.yml - /ci/allure-report.yml @@ -448,23 +448,6 @@ allure-report: ALLURE_MERGE_REQUEST_IID: $CI_MERGE_REQUEST_IID ALLURE_JOB_NAME: e2e-package-and-test -notify-slack: - extends: - - .notify-slack-qa - - .ruby-image - - .bundle-install - - .rules:report:process-results - stage: .post - variables: - ALLURE_JOB_NAME: package-and-e2e - SLACK_ICON_EMOJI: ci_failing - STATUS_SYM: ☠️ - STATUS: failed - when: on_failure - script: - - bundle exec gitlab-qa-report --prepare-stage-reports "$CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.xml" # generate summary - - !reference [.notify-slack-qa, script] - upload-knapsack-report: extends: - .generate-knapsack-report-base @@ -488,7 +471,7 @@ relate-test-failures: script: - | bundle exec gitlab-qa-report \ - --relate-failure-issue "gitlab-qa-run-*/**/rspec-*.json" \ + --relate-failure-issue "$CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json" \ --project "$QA_FAILURES_REPORTING_PROJECT" \ --max-diff-ratio "$QA_FAILURES_MAX_DIFF_RATIO" @@ -505,5 +488,29 @@ generate-test-session: script: - | bundle exec gitlab-qa-report \ - --generate-test-session "gitlab-qa-run-*/**/rspec-*.json" \ + --generate-test-session "$CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.json" \ --project "$QA_TESTCASE_SESSIONS_PROJECT" + artifacts: + when: always + expire_in: 1d + paths: + - qa/REPORT_ISSUE_URL + +notify-slack: + extends: + - .notify-slack-qa + - .ruby-image + - .bundle-install + - .rules:report:process-results + stage: .post + needs: + - generate-test-session + variables: + ALLURE_JOB_NAME: e2e-package-and-test + SLACK_ICON_EMOJI: ci_failing + STATUS_SYM: ☠️ + STATUS: failed + when: on_failure + script: + - bundle exec gitlab-qa-report --prepare-stage-reports "$CI_PROJECT_DIR/gitlab-qa-run-*/**/rspec-*.xml" # generate summary + - !reference [.notify-slack-qa, script] diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 1f37e7f7cd3..6cc958e192d 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -968,6 +968,7 @@ variables: SKIP_REPORT_IN_ISSUES: "false" PROCESS_TEST_RESULTS: "true" + KNAPSACK_GENERATE_REPORT: "true" - <<: *if-force-ci when: manual allow_failure: true diff --git a/.rubocop_todo/style/format_string.yml b/.rubocop_todo/style/format_string.yml index bb95d8f7fe9..66c368a7a52 100644 --- a/.rubocop_todo/style/format_string.yml +++ b/.rubocop_todo/style/format_string.yml @@ -65,6 +65,7 @@ Style/FormatString: - 'app/helpers/members_helper.rb' - 'app/helpers/merge_requests_helper.rb' - 'app/helpers/mirror_helper.rb' + - 'app/helpers/notify_helper.rb' - 'app/helpers/preferences_helper.rb' - 'app/helpers/profiles_helper.rb' - 'app/helpers/projects/project_members_helper.rb' diff --git a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue index c5b713b5447..acc3cbe10a0 100644 --- a/app/assets/javascripts/batch_comments/components/submit_dropdown.vue +++ b/app/assets/javascripts/batch_comments/components/submit_dropdown.vue @@ -82,8 +82,11 @@ export default { this.autosave.reset(); - if (window.mrTabs && this.note) { - window.location.hash = `note_${this.getCurrentUserLastNote.id}`; + if (window.mrTabs && (this.noteData.note || this.noteData.approve)) { + if (this.noteData.note) { + window.location.hash = `note_${this.getCurrentUserLastNote.id}`; + } + window.mrTabs.tabShown('show'); setTimeout(() => diff --git a/app/helpers/notify_helper.rb b/app/helpers/notify_helper.rb index 455b56e92fc..e15b1b21fe1 100644 --- a/app/helpers/notify_helper.rb +++ b/app/helpers/notify_helper.rb @@ -22,7 +22,14 @@ module NotifyHelper end def merge_request_approved_description(merge_request, approved_by) - format(s_('Notify|%{mr_highlight}Merge request%{highlight_end} %{mr_link} %{approved_highlight}was approved by%{highlight_end} %{approver_avatar} %{approver_link}').html_safe, mr_highlight: '<span style="font-weight: 600;color:#333333;">'.html_safe, highlight_end: '</span>'.html_safe, mr_link: link_to(merge_request.to_reference, merge_request_url(merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none").html_safe, approved_highlight: '<span>'.html_safe, approver_avatar: content_tag(:img, nil, height: "24", src: avatar_icon_for_user(approved_by, 24, only_path: false), - style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar", class: "avatar").html_safe, approver_link: link_to(approved_by.name, user_url(approved_by), style: "color:#333333;text-decoration:none;", class: "muted").html_safe) + s_('Notify|%{mr_highlight}Merge request%{highlight_end} %{mr_link} %{approved_highlight}was approved by%{highlight_end} %{approver_avatar} %{approver_link}') + .html_safe % { + mr_highlight: '<span style="font-weight: 600;color:#333333;">'.html_safe, + highlight_end: '</span>'.html_safe, + mr_link: link_to(merge_request.to_reference, merge_request_url(merge_request), style: "font-weight: 600;color:#3777b0;text-decoration:none").html_safe, + approved_highlight: '<span>'.html_safe, + approver_avatar: content_tag(:img, nil, height: "24", src: avatar_icon_for_user(approved_by, 24, only_path: false), style: "border-radius:12px;margin:-7px 0 -7px 3px;", width: "24", alt: "Avatar", class: "avatar").html_safe, + approver_link: link_to(approved_by.name, user_url(approved_by), style: "color:#333333;text-decoration:none;", class: "muted").html_safe + } end end diff --git a/app/models/concerns/ci/artifactable.rb b/app/models/concerns/ci/artifactable.rb index ee8e98ec1bf..3fdbd6a8789 100644 --- a/app/models/concerns/ci/artifactable.rb +++ b/app/models/concerns/ci/artifactable.rb @@ -10,8 +10,17 @@ module Ci STORE_COLUMN = :file_store NotSupportedAdapterError = Class.new(StandardError) FILE_FORMAT_ADAPTERS = { + # While zip is a streamable file format, performing streaming + # reads requires that each entry in the zip has certain headers + # present at the front of the entry. These headers are OPTIONAL + # according to the file format specification. GitLab Runner uses + # Go's `archive/zip` to create zip archives, which does not include + # these headers. Go maintainers have expressed that they don't intend + # to support them: https://github.com/golang/go/issues/23301#issuecomment-363240781 + # + # If you need GitLab to be able to read Artifactables, store them in + # raw or gzip format instead of zip. gzip: Gitlab::Ci::Build::Artifacts::Adapters::GzipStream, - zip: Gitlab::Ci::Build::Artifacts::Adapters::ZipStream, raw: Gitlab::Ci::Build::Artifacts::Adapters::RawStream }.freeze diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index dfa9316889e..5cd390141f0 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -35,12 +35,14 @@ module Users return user end - # Calling all before/after_destroy hooks for the user because - # there is no dependent: destroy in the relationship. And the removal - # is done by a foreign_key. Otherwise they won't be called - user.members.find_each { |member| member.run_callbacks(:destroy) } + user.block - user.solo_owned_groups.each do |group| + # Load the records. Groups are unavailable after membership is destroyed. + solo_owned_groups = user.solo_owned_groups.load + + user.members.each_batch { |batch| batch.destroy_all } # rubocop:disable Style/SymbolProc, Cop/DestroyAll + + solo_owned_groups.each do |group| Groups::DestroyService.new(group, current_user).execute end diff --git a/app/views/notify/new_mention_in_issue_email.html.haml b/app/views/notify/new_mention_in_issue_email.html.haml index 6b45ac265f7..3b2e36d118b 100644 --- a/app/views/notify/new_mention_in_issue_email.html.haml +++ b/app/views/notify/new_mention_in_issue_email.html.haml @@ -1,4 +1,4 @@ %p - You have been mentioned in an issue. + = s_('Notify|You have been mentioned in an issue.') = render template: 'notify/new_issue_email' diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml index 4a21cb32c20..1409b28e735 100644 --- a/app/views/projects/_stat_anchor_list.html.haml +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -2,7 +2,7 @@ - project_buttons = local_assigns.fetch(:project_buttons, false) - return unless anchors.any? -%ul.nav.gl-gap-3 +%ul.nav{ class: (project_buttons ? 'gl-gap-3' : 'gl-gap-5') } - anchors.each do |anchor| %li.nav-item = link_to_if(anchor.link, anchor.label, anchor.link, stat_anchor_attrs(anchor)) do diff --git a/danger/config_files/Dangerfile b/danger/config_files/Dangerfile new file mode 100644 index 00000000000..dcd2e44df07 --- /dev/null +++ b/danger/config_files/Dangerfile @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +config_files.add_suggestion_for_missing_introduced_by_url diff --git a/danger/plugins/config_files.rb b/danger/plugins/config_files.rb new file mode 100644 index 00000000000..2604a491d03 --- /dev/null +++ b/danger/plugins/config_files.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative '../../tooling/danger/config_files' + +module Danger + class ConfigFiles < ::Danger::Plugin + # Put the helper code somewhere it can be tested + include Tooling::Danger::ConfigFiles + end +end diff --git a/db/post_migrate/20220826165048_drop_temporary_job_trace_index.rb b/db/post_migrate/20220826165048_drop_temporary_job_trace_index.rb new file mode 100644 index 00000000000..0cad7cd1968 --- /dev/null +++ b/db/post_migrate/20220826165048_drop_temporary_job_trace_index.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class DropTemporaryJobTraceIndex < Gitlab::Database::Migration[2.0] + INDEX_NAME = 'tmp_index_ci_job_artifacts_on_id_where_trace_and_expire_at' + + def up + prepare_async_index_removal :ci_job_artifacts, :id, name: INDEX_NAME + end + + def down + unprepare_async_index_by_name :ci_job_artifacts, INDEX_NAME + end +end diff --git a/db/schema_migrations/20220826165048 b/db/schema_migrations/20220826165048 new file mode 100644 index 00000000000..0539118356d --- /dev/null +++ b/db/schema_migrations/20220826165048 @@ -0,0 +1 @@ +75cb9d7b4a0bc8ad26b3bf6bf41a4414bcc4307607de058fc35fe4ece7009423
\ No newline at end of file diff --git a/doc/api/group_protected_environments.md b/doc/api/group_protected_environments.md index 0f1527f8968..8a7a3ecd3bc 100644 --- a/doc/api/group_protected_environments.md +++ b/doc/api/group_protected_environments.md @@ -48,13 +48,14 @@ Example response: "name":"production", "deploy_access_levels":[ { + "id": 12, "access_level": 40, "access_level_description": "Maintainers", "user_id": null, "group_id": null } ], - "required_approval_count": 0 + "required_approval_count": 0 } ] ``` @@ -83,6 +84,7 @@ Example response: "name":"production", "deploy_access_levels":[ { + "id": 12, "access_level":40, "access_level_description":"Maintainers", "user_id":null, @@ -123,13 +125,182 @@ Example response: "name":"production", "deploy_access_levels":[ { + "id": 12, "access_level": 40, "access_level_description": "protected-access-group", "user_id": null, "group_id": 9899826 } ], - "required_approval_count": 0 + "required_approval_count": 0 +} +``` + +## Update an environment + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351854) in GitLab 15.4. + +Updates a single environment. + +```plaintext +PUT /groups/:id/protected_environments/:name +``` + +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) maintained by the authenticated user. | +| `name` | string | yes | The deployment tier of the protected environment. One of `production`, `staging`, `testing`, `development`, or `other`. Read more about [deployment tiers](../ci/environments/index.md#deployment-tier-of-environments).| +| `deploy_access_levels` | array | no | Array of access levels allowed to deploy, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. | +| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. | +| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. One of `user_id`, `group_id` or `access_level`. They take the form of `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}` respectively. You can also specify the number of required approvals from the specified entity with `required_approvals` field. See [Multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) for more information. | + +To update: + +- **`user_id`**: Ensure the updated user belongs to the given group with the Maintainer role (or above). You must also pass the `id` of either a `deploy_access_level` or `approval_rule` in the respective hash. +- **`group_id`**: Ensure the updated group is a sub-group of the group this protected environment belongs to. You must also pass the `id` of either a `deploy_access_level` or `approval_rule` in the respective hash. + +To delete: + +- You must pass `_destroy` set to `true`. See the following examples. + +### Example: Create a `deploy_access_level` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"deploy_access_levels": [{"group_id": 9899829, access_level: 40}], "required_approval_count": 1}' \ + --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/22034114/protected_environments/production" +``` + +Example response: + +```json +{ + "name": "production", + "deploy_access_levels": [ + { + "id": 12, + "access_level": 40, + "access_level_description": "protected-access-group", + "user_id": null, + "group_id": 9899829, + "group_inheritance_type": 1 + } + ], + "required_approval_count": 0 +} +``` + +### Example: Update a `deploy_access_level` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"deploy_access_levels": [{"id": 12, "group_id": 22034120}], "required_approval_count": 2}' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_environments/production" +``` + +```json +{ + "name": "production", + "deploy_access_levels": [ + { + "id": 12, + "access_level": 40, + "access_level_description": "protected-access-group", + "user_id": null, + "group_id": 22034120, + "group_inheritance_type": 0 + } + ], + "required_approval_count": 2 +} +``` + +### Example: Delete a `deploy_access_level` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"deploy_access_levels": [{"id": 12, "_destroy": true}], "required_approval_count": 0}' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_environments/production" +``` + +Example response: + +```json +{ + "name": "production", + "deploy_access_levels": [], + "required_approval_count": 0 +} +``` + +### Example: Create an `approval_rule` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"approval_rules": [{"group_id": 134, "required_approvals": 1}]}' \ + --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/groups/22034114/protected_environments/production" +``` + +Example response: + +```json +{ + "name": "production", + "approval_rules": [ + { + "id": 38, + "user_id": null, + "group_id": 134, + "access_level": null, + "access_level_description": "qa-group", + "required_approvals": 1, + "group_inheritance_type": 0 + } + ] +} +``` + +### Example: Update an `approval_rule` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"approval_rules": [{"id": 38, "group_id": 135, "required_approvals": 2}]}' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_environments/production" +``` + +```json +{ + "name": "production", + "approval_rules": [ + { + "id": 38, + "user_id": null, + "group_id": 135, + "access_level": null, + "access_level_description": "security-group", + "required_approvals": 2, + "group_inheritance_type": 0 + } + ] +} +``` + +### Example: Delete an `approval_rule` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"approval_rules": [{"id": 38, "_destroy": true}]}' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/22034114/protected_environments/production" +``` + +Example response: + +```json +{ + "name": "production", + "approval_rules": [] } ``` diff --git a/doc/api/protected_environments.md b/doc/api/protected_environments.md index d210a6dfccf..4c6f509a752 100644 --- a/doc/api/protected_environments.md +++ b/doc/api/protected_environments.md @@ -54,6 +54,7 @@ Example response: "name":"production", "deploy_access_levels":[ { + "id": 12, "access_level":40, "access_level_description":"Maintainers", "user_id":null, @@ -61,7 +62,7 @@ Example response: "group_inheritance_type": 0 } ], - "required_approval_count": 0 + "required_approval_count": 0 } ] ``` @@ -90,6 +91,7 @@ Example response: "name":"production", "deploy_access_levels":[ { + "id": 12, "access_level": 40, "access_level_description": "Maintainers", "user_id": null, @@ -97,7 +99,7 @@ Example response: "group_inheritance_type": 0 } ], - "required_approval_count": 0 + "required_approval_count": 0 } ``` @@ -109,6 +111,20 @@ Protects a single environment: POST /projects/:id/protected_environments ``` +| Attribute | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | +| `name` | string | yes | The name of the environment. | +| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. | +| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. | +| `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. See [Multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) for more information. | + +Elements in the `deploy_access_levels` and `approval_rules` array should be one of `user_id`, `group_id` or +`access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or +`{access_level: integer}`. Optionally you can specify the `group_inheritance_type` on each as one of the [valid group inheritance types](#group-inheritance-types). + +Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). + ```shell curl --header 'Content-Type: application/json' --request POST \ --data '{"name": "production", "deploy_access_levels": [{"group_id": 9899826}], "approval_rules": [{"group_id": 134}, {"group_id": 135, "required_approvals": 2}]}' \ @@ -116,19 +132,84 @@ curl --header 'Content-Type: application/json' --request POST \ "https://gitlab.example.com/api/v4/projects/22034114/protected_environments" ``` +Example response: + +```json +{ + "name": "production", + "deploy_access_levels": [ + { + "id": 12, + "access_level": 40, + "access_level_description": "protected-access-group", + "user_id": null, + "group_id": 9899826, + "group_inheritance_type": 0 + } + ], + "required_approval_count": 0, + "approval_rules": [ + { + "id": 38, + "user_id": null, + "group_id": 134, + "access_level": null, + "access_level_description": "qa-group", + "required_approvals": 1, + "group_inheritance_type": 0 + }, + { + "id": 39, + "user_id": null, + "group_id": 135, + "access_level": null, + "access_level_description": "security-group", + "required_approvals": 2, + "group_inheritance_type": 0 + } + ] +} +``` + +## Update a protected environment + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351854) in GitLab 15.4. + +Updates a single environment. + +```plaintext +PUT /projects/:id/protected_environments/:name +``` + | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | | `name` | string | yes | The name of the environment. | -| `deploy_access_levels` | array | yes | Array of access levels allowed to deploy, with each described by a hash. | -| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. | +| `deploy_access_levels` | array | no | Array of access levels allowed to deploy, with each described by a hash. | +| `required_approval_count` | integer | no | The number of approvals required to deploy to this environment. | | `approval_rules` | array | no | Array of access levels allowed to approve, with each described by a hash. See [Multiple approval rules](../ci/environments/deployment_approvals.md#multiple-approval-rules) for more information. | Elements in the `deploy_access_levels` and `approval_rules` array should be one of `user_id`, `group_id` or `access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. Optionally you can specify the `group_inheritance_type` on each as one of the [valid group inheritance types](#group-inheritance-types). -Each user must have access to the project and each group must [have this project shared](../user/project/members/share_project_with_groups.md). +To update: + +- **`user_id`**: Ensure the updated user has access to the project. You must also pass the `id` of either a `deploy_access_level` or `approval_rule` in the respective hash. +- **`group_id`**: Ensure the updated group [have this project shared](../user/project/members/share_project_with_groups.md). You must also pass the `id` of either a `deploy_access_level` or `approval_rule` in the respective hash. + +To delete: + +- You must pass `_destroy` set to `true`. See the following examples. + +### Example: Create a `deploy_access_level` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"deploy_access_levels": [{"group_id": 9899829, access_level: 40}], "required_approval_count": 1}' \ + --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/projects/22034114/protected_environments/production" +``` Example response: @@ -137,32 +218,128 @@ Example response: "name": "production", "deploy_access_levels": [ { + "id": 12, "access_level": 40, "access_level_description": "protected-access-group", "user_id": null, - "group_id": 9899826, + "group_id": 9899829, + "group_inheritance_type": 1 + } + ], + "required_approval_count": 0 +} +``` + +### Example: Update a `deploy_access_level` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"deploy_access_levels": [{"id": 12, "group_id": 22034120}], "required_approval_count": 2}' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_environments/production" +``` + +```json +{ + "name": "production", + "deploy_access_levels": [ + { + "id": 12, + "access_level": 40, + "access_level_description": "protected-access-group", + "user_id": null, + "group_id": 22034120, "group_inheritance_type": 0 } ], - "required_approval_count": 0, - "approval_rules": [ - { - "user_id": null, - "group_id": 134, - "access_level": null, - "access_level_description": "qa-group", - "required_approvals": 1, - "group_inheritance_type": 0 - }, - { - "user_id": null, - "group_id": 135, - "access_level": null, - "access_level_description": "security-group", - "required_approvals": 2, - "group_inheritance_type": 0 - } - ] + "required_approval_count": 2 +} +``` + +### Example: Delete a `deploy_access_level` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"deploy_access_levels": [{"id": 12, "_destroy": true}], "required_approval_count": 0}' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_environments/production" +``` + +Example response: + +```json +{ + "name": "production", + "deploy_access_levels": [], + "required_approval_count": 0 +} +``` + +### Example: Create an `approval_rule` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"approval_rules": [{"group_id": 134, "required_approvals": 1}]}' \ + --header "PRIVATE-TOKEN: <your_access_token>" \ + "https://gitlab.example.com/api/v4/projects/22034114/protected_environments/production" +``` + +Example response: + +```json +{ + "name": "production", + "approval_rules": [ + { + "id": 38, + "user_id": null, + "group_id": 134, + "access_level": null, + "access_level_description": "qa-group", + "required_approvals": 1, + "group_inheritance_type": 0 + } + ] +} +``` + +### Example: Update an `approval_rule` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"approval_rules": [{"id": 38, "group_id": 135, "required_approvals": 2}]}' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_environments/production" +``` + +```json +{ + "name": "production", + "approval_rules": [ + { + "id": 38, + "user_id": null, + "group_id": 135, + "access_level": null, + "access_level_description": "security-group", + "required_approvals": 2, + "group_inheritance_type": 0 + } + ] +} +``` + +### Example: Delete an `approval_rule` record + +```shell +curl --header 'Content-Type: application/json' --request PUT \ + --data '{"approval_rules": [{"id": 38, "_destroy": true}]}' \ + --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/22034114/protected_environments/production" +``` + +Example response: + +```json +{ + "name": "production", + "approval_rules": [] } ``` @@ -174,11 +351,11 @@ Unprotects the given protected environment: DELETE /projects/:id/protected_environments/:name ``` -```shell -curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_environments/staging" -``` - | Attribute | Type | Required | Description | | --------- | ---- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user. | | `name` | string | yes | The name of the protected environment. | + +```shell +curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_environments/staging" +``` diff --git a/doc/development/sec/index.md b/doc/development/sec/index.md index 9200311f731..06c20cee0bb 100644 --- a/doc/development/sec/index.md +++ b/doc/development/sec/index.md @@ -5,9 +5,10 @@ info: To determine the technical writer assigned to the Stage/Group associated w type: index, concepts, howto --- -# Sec Section development documentation **(FREE)** +# Sec section development **(FREE)** -Development guides that are specific to Sec Section are listed here. +The Sec section is responsible for GitLab application security features, the "Sec" part of +DevSecOps. Development guides that are specific to the Sec section are listed here. See [Terminology](../../user/application_security/terminology) for an overview of our shared terminology. diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md index 1196fe3c524..ee161deaabb 100644 --- a/doc/user/project/members/share_project_with_groups.md +++ b/doc/user/project/members/share_project_with_groups.md @@ -48,6 +48,7 @@ After sharing 'Project Acme' with 'Engineering': - The group is listed in the **Groups** tab. - The project is listed on the group dashboard. +- All members, including members from the ancestors of the 'Engineering' group, gain access to 'Project Acme' with an access level based on the outcome of [maximum access level](#maximum-access-level). When you share a project, be aware of the following restrictions and outcomes: diff --git a/lib/gitlab/ci/build/artifacts/adapters/zip_stream.rb b/lib/gitlab/ci/build/artifacts/adapters/zip_stream.rb deleted file mode 100644 index 690a47097c6..00000000000 --- a/lib/gitlab/ci/build/artifacts/adapters/zip_stream.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module Build - module Artifacts - module Adapters - class ZipStream - MAX_DECOMPRESSED_SIZE = 100.megabytes - MAX_FILES_PROCESSED = 50 - - attr_reader :stream - - InvalidStreamError = Class.new(StandardError) - - def initialize(stream) - raise InvalidStreamError, "Stream is required" unless stream - - @stream = stream - @files_processed = 0 - end - - def each_blob - Zip::InputStream.open(stream) do |zio| - while entry = zio.get_next_entry - break if at_files_processed_limit? - next unless should_process?(entry) - - @files_processed += 1 - - yield entry.get_input_stream.read - end - end - end - - private - - def should_process?(entry) - file?(entry) && !too_large?(entry) - end - - def file?(entry) - # Check the file name as a workaround for incorrect - # file type detection when using InputStream - # https://github.com/rubyzip/rubyzip/issues/533 - entry.file? && !entry.name.end_with?('/') - end - - def too_large?(entry) - entry.size > MAX_DECOMPRESSED_SIZE - end - - def at_files_processed_limit? - @files_processed >= MAX_FILES_PROCESSED - end - end - end - end - end - end -end diff --git a/lib/gitlab/view/presenter/base.rb b/lib/gitlab/view/presenter/base.rb index a2d217fb42f..2a57ca9ae02 100644 --- a/lib/gitlab/view/presenter/base.rb +++ b/lib/gitlab/view/presenter/base.rb @@ -46,6 +46,13 @@ module Gitlab url_builder.build(__subject__, only_path: true) end + def path_with_line_numbers(path, start_line, end_line) + path.tap do |complete_path| + complete_path << "#L#{start_line}" + complete_path << "-#{end_line}" if end_line && end_line != start_line + end + end + class_methods do def presenter? true diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 995284354fc..1192b788a06 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -26950,6 +26950,9 @@ msgstr "" msgid "Notify|You don't have access to the project." msgstr "" +msgid "Notify|You have been mentioned in an issue." +msgstr "" + msgid "Notify|Your request to join the %{target_to_join} %{target_type} has been %{denied_tag}." msgstr "" @@ -40076,6 +40079,9 @@ msgstr "" msgid "This board's scope is reduced" msgstr "" +msgid "This change will remove %{strongOpen}ALL%{strongClose} Premium and Ultimate features for %{strongOpen}ALL%{strongClose} SaaS customers and make tests start failing." +msgstr "" + msgid "This chart could not be displayed" msgstr "" diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb index e35828ecd6a..f9d55c0009c 100644 --- a/qa/qa/page/project/sub_menus/repository.rb +++ b/qa/qa/page/project/sub_menus/repository.rb @@ -37,6 +37,14 @@ module QA end end + def go_to_repository_contributors + hover_repository do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Contributors') + end + end + end + private def hover_repository diff --git a/scripts/generate-e2e-pipeline b/scripts/generate-e2e-pipeline index 0fa940d69af..0588b923b3b 100755 --- a/scripts/generate-e2e-pipeline +++ b/scripts/generate-e2e-pipeline @@ -17,6 +17,7 @@ variables=$(cat <<YML variables: RELEASE: "${CI_REGISTRY}/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:${CI_MERGE_REQUEST_SOURCE_BRANCH_SHA:-$CI_COMMIT_SHA}" SKIP_REPORT_IN_ISSUES: "${SKIP_REPORT_IN_ISSUES:-true}" + OMNIBUS_GITLAB_CACHE_UPDATE: "${OMNIBUS_GITLAB_CACHE_UPDATE:-false}" COLORIZED_LOGS: "true" QA_LOG_LEVEL: "info" QA_TESTS: "$QA_TESTS" diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb index 07cc30486e0..f8b964cf8e0 100644 --- a/spec/factories/ci/job_artifacts.rb +++ b/spec/factories/ci/job_artifacts.rb @@ -102,28 +102,6 @@ FactoryBot.define do end end - trait :zip_with_single_file do - file_type { :archive } - file_format { :zip } - - after(:build) do |artifact, evaluator| - artifact.file = fixture_file_upload( - Rails.root.join('spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/single_file.zip'), - 'application/zip') - end - end - - trait :zip_with_multiple_files do - file_type { :archive } - file_format { :zip } - - after(:build) do |artifact, evaluator| - artifact.file = fixture_file_upload( - Rails.root.join('spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/multiple_files.zip'), - 'application/zip') - end - end - trait :junit do file_type { :junit } file_format { :gzip } diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/100_files.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/100_files.zip Binary files differdeleted file mode 100644 index 31124abc0e5..00000000000 --- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/100_files.zip +++ /dev/null diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/200_mb_decompressed.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/200_mb_decompressed.zip Binary files differdeleted file mode 100644 index 8c56cce641a..00000000000 --- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/200_mb_decompressed.zip +++ /dev/null diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/multiple_files.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/multiple_files.zip Binary files differdeleted file mode 100644 index 09ac4e5df51..00000000000 --- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/multiple_files.zip +++ /dev/null diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/single_file.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/single_file.zip Binary files differdeleted file mode 100644 index 81768a9f2b3..00000000000 --- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/single_file.zip +++ /dev/null diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/with_directory.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/with_directory.zip Binary files differdeleted file mode 100644 index 6de321ea86a..00000000000 --- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/with_directory.zip +++ /dev/null diff --git a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/zipbomb.zip b/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/zipbomb.zip Binary files differdeleted file mode 100644 index b8cfcef9739..00000000000 --- a/spec/fixtures/lib/gitlab/ci/build/artifacts/adapters/zip_stream/zipbomb.zip +++ /dev/null diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js index dc7ecb8e44d..b28e6d68e40 100644 --- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js +++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js @@ -23,6 +23,7 @@ function factory({ canApprove = true } = {}) { current_user: { can_approve: canApprove }, }), noteableType: () => 'merge_request', + getCurrentUserLastNote: () => ({ id: 1 }), }, modules: { batchComments: { @@ -45,6 +46,7 @@ const findForm = () => wrapper.findByTestId('submit-gl-form'); describe('Batch comments submit dropdown', () => { afterEach(() => { wrapper.destroy(); + window.mrTabs = null; }); it('calls publishReview with note data', async () => { @@ -63,6 +65,19 @@ describe('Batch comments submit dropdown', () => { }); }); + it('switches to the overview tab after submit', async () => { + window.mrTabs = { tabShown: jest.fn() }; + + factory(); + + findCommentTextarea().setValue('Hello world'); + + await findForm().vm.$emit('submit', { preventDefault: jest.fn() }); + await Vue.nextTick(); + + expect(window.mrTabs.tabShown).toHaveBeenCalledWith('show'); + }); + it('sets submit dropdown to loading', async () => { factory(); diff --git a/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb b/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb deleted file mode 100644 index 2c236ba3726..00000000000 --- a/spec/lib/gitlab/ci/build/artifacts/adapters/zip_stream_spec.rb +++ /dev/null @@ -1,86 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Ci::Build::Artifacts::Adapters::ZipStream do - let(:file_name) { 'single_file.zip' } - let(:fixture_path) { "lib/gitlab/ci/build/artifacts/adapters/zip_stream/#{file_name}" } - let(:stream) { File.open(expand_fixture_path(fixture_path), 'rb') } - - describe '#initialize' do - it 'initializes when stream is passed' do - expect { described_class.new(stream) }.not_to raise_error - end - - context 'when stream is not passed' do - let(:stream) { nil } - - it 'raises an error' do - expect { described_class.new(stream) }.to raise_error(described_class::InvalidStreamError) - end - end - end - - describe '#each_blob' do - let(:adapter) { described_class.new(stream) } - - context 'when stream is a zip file' do - it 'iterates file content when zip file contains one file' do - expect { |b| adapter.each_blob(&b) } - .to yield_with_args("file 1 content\n") - end - - context 'when zip file contains multiple files' do - let(:file_name) { 'multiple_files.zip' } - - it 'iterates content of all files' do - expect { |b| adapter.each_blob(&b) } - .to yield_successive_args("file 1 content\n", "file 2 content\n") - end - end - - context 'when zip file includes files in a directory' do - let(:file_name) { 'with_directory.zip' } - - it 'iterates contents from files only' do - expect { |b| adapter.each_blob(&b) } - .to yield_successive_args("file 1 content\n", "file 2 content\n") - end - end - - context 'when zip contains a file which decompresses beyond the size limit' do - let(:file_name) { '200_mb_decompressed.zip' } - - it 'does not read the file' do - expect { |b| adapter.each_blob(&b) }.not_to yield_control - end - end - - context 'when the zip contains too many files' do - let(:file_name) { '100_files.zip' } - - it 'stops processing when the limit is reached' do - expect { |b| adapter.each_blob(&b) } - .to yield_control.exactly(described_class::MAX_FILES_PROCESSED).times - end - end - - context 'when stream is a zipbomb' do - let(:file_name) { 'zipbomb.zip' } - - it 'does not read the file' do - expect { |b| adapter.each_blob(&b) }.not_to yield_control - end - end - end - - context 'when stream is not a zip file' do - let(:stream) { File.open(expand_fixture_path('junit/junit.xml.gz'), 'rb') } - - it 'does not yield any data' do - expect { |b| adapter.each_blob(&b) }.not_to yield_control - expect { adapter.each_blob { |b| b } }.not_to raise_error - end - end - end -end diff --git a/spec/models/commit_signatures/gpg_signature_spec.rb b/spec/models/commit_signatures/gpg_signature_spec.rb index 6ae2a202b72..605ad725dd7 100644 --- a/spec/models/commit_signatures/gpg_signature_spec.rb +++ b/spec/models/commit_signatures/gpg_signature_spec.rb @@ -5,12 +5,14 @@ require 'spec_helper' RSpec.describe CommitSignatures::GpgSignature do # This commit is seeded from https://gitlab.com/gitlab-org/gitlab-test # For instructions on how to add more seed data, see the project README - let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } - let!(:project) { create(:project, :repository, path: 'sample-project') } - let!(:commit) { create(:commit, project: project, sha: commit_sha) } - let(:signature) { create(:gpg_signature, commit_sha: commit_sha) } - let(:gpg_key) { create(:gpg_key) } - let(:gpg_key_subkey) { create(:gpg_key_subkey) } + let_it_be(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } + let_it_be(:project) { create(:project, :repository, path: 'sample-project') } + let_it_be(:commit) { create(:commit, project: project, sha: commit_sha) } + let_it_be(:gpg_key) { create(:gpg_key) } + let_it_be(:gpg_key_subkey) { create(:gpg_key_subkey, gpg_key: gpg_key) } + + let(:signature) { create(:gpg_signature, commit_sha: commit_sha, gpg_key: gpg_key) } + let(:attributes) do { commit_sha: commit_sha, @@ -35,8 +37,7 @@ RSpec.describe CommitSignatures::GpgSignature do end describe '.by_commit_sha scope' do - let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) } - let!(:another_gpg_signature) { create(:gpg_signature, gpg_key: gpg_key) } + let_it_be(:another_gpg_signature) { create(:gpg_signature, gpg_key: gpg_key) } it 'returns all gpg signatures by sha' do expect(described_class.by_commit_sha(commit_sha)).to match_array([signature]) diff --git a/spec/models/commit_signatures/ssh_signature_spec.rb b/spec/models/commit_signatures/ssh_signature_spec.rb index 64d95fe3a71..08530bf6964 100644 --- a/spec/models/commit_signatures/ssh_signature_spec.rb +++ b/spec/models/commit_signatures/ssh_signature_spec.rb @@ -5,11 +5,11 @@ require 'spec_helper' RSpec.describe CommitSignatures::SshSignature do # This commit is seeded from https://gitlab.com/gitlab-org/gitlab-test # For instructions on how to add more seed data, see the project README - let(:commit_sha) { '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' } - let!(:project) { create(:project, :repository, path: 'sample-project') } - let!(:commit) { create(:commit, project: project, sha: commit_sha) } - let(:signature) { create(:ssh_signature, commit_sha: commit_sha) } - let(:ssh_key) { create(:ed25519_key_256) } + let_it_be(:commit_sha) { '7b5160f9bb23a3d58a0accdbe89da13b96b1ece9' } + let_it_be(:project) { create(:project, :repository, path: 'sample-project') } + let_it_be(:commit) { create(:commit, project: project, sha: commit_sha) } + let_it_be(:ssh_key) { create(:ed25519_key_256) } + let(:attributes) do { commit_sha: commit_sha, @@ -18,6 +18,8 @@ RSpec.describe CommitSignatures::SshSignature do } end + let(:signature) { create(:ssh_signature, commit_sha: commit_sha, key: ssh_key) } + it_behaves_like 'having unique enum values' it_behaves_like 'commit signature' diff --git a/spec/models/commit_signatures/x509_commit_signature_spec.rb b/spec/models/commit_signatures/x509_commit_signature_spec.rb index beb101cdd89..b971fd078e2 100644 --- a/spec/models/commit_signatures/x509_commit_signature_spec.rb +++ b/spec/models/commit_signatures/x509_commit_signature_spec.rb @@ -5,11 +5,10 @@ require 'spec_helper' RSpec.describe CommitSignatures::X509CommitSignature do # This commit is seeded from https://gitlab.com/gitlab-org/gitlab-test # For instructions on how to add more seed data, see the project README - let(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' } - let(:project) { create(:project, :public, :repository) } - let!(:commit) { create(:commit, project: project, sha: commit_sha) } - let(:x509_certificate) { create(:x509_certificate) } - let(:signature) { create(:x509_commit_signature, commit_sha: commit_sha) } + let_it_be(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:commit) { create(:commit, project: project, sha: commit_sha) } + let_it_be(:x509_certificate) { create(:x509_certificate) } let(:attributes) do { @@ -20,6 +19,8 @@ RSpec.describe CommitSignatures::X509CommitSignature do } end + let(:signature) { create(:x509_commit_signature, commit_sha: commit_sha, x509_certificate: x509_certificate) } + it_behaves_like 'having unique enum values' it_behaves_like 'commit signature' diff --git a/spec/models/concerns/ci/artifactable_spec.rb b/spec/models/concerns/ci/artifactable_spec.rb index 64691165e21..6af244a5a0f 100644 --- a/spec/models/concerns/ci/artifactable_spec.rb +++ b/spec/models/concerns/ci/artifactable_spec.rb @@ -46,30 +46,8 @@ RSpec.describe Ci::Artifactable do end end - context 'when file format is zip' do - context 'when artifact contains one file' do - let(:artifact) { build(:ci_job_artifact, :zip_with_single_file) } - - it 'iterates blob once' do - expect { |b| artifact.each_blob(&b) }.to yield_control.once - end - end - - context 'when artifact contains two files' do - let(:artifact) { build(:ci_job_artifact, :zip_with_multiple_files) } - - it 'iterates blob two times' do - expect { |b| artifact.each_blob(&b) }.to yield_control.exactly(2).times - end - end - end - context 'when there are no adapters for the file format' do - let(:artifact) { build(:ci_job_artifact, :junit) } - - before do - allow(artifact).to receive(:file_format).and_return(:unknown) - end + let(:artifact) { build(:ci_job_artifact, :junit, file_format: :zip) } it 'raises an error' do expect { |b| artifact.each_blob(&b) }.to raise_error(described_class::NotSupportedAdapterError) diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index 90c4f70d749..26e25cf6438 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -190,9 +190,20 @@ RSpec.describe Users::DestroyService do ]) end - it 'does not delete the user' do + it 'does not delete the user, nor the group' do service.execute(user) + expect(User.find(user.id)).to eq user + expect(Group.find(solo_owned.id)).to eq solo_owned + end + + context 'when delete solo owned groups option is passed' do + it 'deletes the user and the group' do + service.execute(user, delete_solo_owned_groups: true) + + expect(User.where(id: user.id)).not_to exist + expect(Group.where(id: solo_owned.id)).not_to exist + end end end diff --git a/spec/tooling/danger/config_files_spec.rb b/spec/tooling/danger/config_files_spec.rb new file mode 100644 index 00000000000..0e01908a1dd --- /dev/null +++ b/spec/tooling/danger/config_files_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'gitlab-dangerfiles' +require 'danger' +require 'danger/plugins/internal/helper' +require 'gitlab/dangerfiles/spec_helper' + +require_relative '../../../tooling/danger/config_files' +require_relative '../../../tooling/danger/project_helper' + +RSpec.describe Tooling::Danger::ConfigFiles do + include_context "with dangerfile" + + let(:fake_danger) { DangerSpecHelper.fake_danger.include(described_class) } + let(:fake_project_helper) { instance_double(Tooling::Danger::ProjectHelper) } + let(:matching_line) { "+ introduced_by_url:" } + + subject(:config_file) { fake_danger.new(helper: fake_helper) } + + before do + allow(config_file).to receive(:project_helper).and_return(fake_project_helper) + end + + describe '#add_suggestion_for_missing_introduced_by_url' do + let(:file_lines) do + [ + "---", + "name: about_your_company_registration_flow", + "introduced_by_url: #{url}", + "rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/355909", + "milestone: '14.10'" + ] + end + + let(:filename) { 'config/feature_flags/new_ff.yml' } + + before do + allow(config_file.project_helper).to receive(:file_lines).and_return(file_lines) + allow(config_file.helper).to receive(:added_files).and_return([filename]) + allow(config_file.helper).to receive(:mr_web_url).and_return(url) + end + + context 'when config file has an empty introduced_by_url line' do + let(:url) { '' } + + it 'adds suggestions at the correct line' do + expected_format = format(described_class::SUGGEST_INTRODUCED_BY_COMMENT, url: url) + expect(config_file).to receive(:markdown).with(expected_format, file: filename, line: 3) + + config_file.add_suggestion_for_missing_introduced_by_url + end + end + + context 'when config file has an introduced_by_url line with value' do + let(:url) { 'https://gitlab.com/gitlab-org/gitlab/-/issues/1' } + + it 'does not add suggestion' do + expect(config_file).not_to receive(:markdown) + + config_file.add_suggestion_for_missing_introduced_by_url + end + end + end + + describe '#new_config_files' do + let(:expected_files) do + %w[ + config/feature_flags/first.yml + config/events/1234_new_event.yml + config/metrics/count_7d/new_metric.yml + ] + end + + before do + all_new_files = %w[ + app/workers/a.rb + doc/events/new_event.md + config/feature_flags/first.yml + config/events/1234_new_event.yml + config/metrics/count_7d/new_metric.yml + app/assets/index.js + ] + + allow(config_file.helper).to receive(:added_files).and_return(all_new_files) + end + + it 'returns added, modified, and renamed_after files by default' do + expect(config_file.new_config_files).to match_array(expected_files) + end + end +end diff --git a/tooling/danger/config_files.rb b/tooling/danger/config_files.rb new file mode 100644 index 00000000000..436335bfc06 --- /dev/null +++ b/tooling/danger/config_files.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'yaml' + +module Tooling + module Danger + module ConfigFiles + SUGGEST_INTRODUCED_BY_COMMENT = <<~SUGGEST_COMMENT + ```suggestion + introduced_by_url: "%<url>s" + ``` + SUGGEST_COMMENT + + CONFIG_DIRS = %w[ + config/feature_flags + config/metrics + config/events + ].freeze + + def add_suggestion_for_missing_introduced_by_url + new_config_files.each do |file_name| + config_file_lines = project_helper.file_lines(file_name) + + config_file_lines.each_with_index do |added_line, i| + next unless added_line =~ /^introduced_by_url:\s?$/ + + markdown(format(SUGGEST_INTRODUCED_BY_COMMENT, url: helper.mr_web_url), file: file_name, line: i + 1) + end + end + end + + def new_config_files + helper.added_files.select { |f| in_config_dir?(f) && f.end_with?('yml') } + end + + private + + def in_config_dir?(path) + CONFIG_DIRS.any? { |d| path.start_with?(d) } + end + end + end +end |