From defc424997d8329613ef3951ab30adf6b3b94f01 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 11 May 2022 03:07:57 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo.yml | 7 - .../string_literals_in_interpolation.yml | 69 ++++++++++ .../file_nav/pipeline_editor_file_nav.vue | 6 +- .../components/file_tree/container.vue | 38 +++++- .../components/file_tree/file_item.vue | 31 ++++- .../javascripts/pipeline_editor/constants.js | 1 + .../graphql/queries/ci_config.query.graphql | 6 + .../pipeline_editor/pipeline_editor_home.vue | 9 +- .../page_bundles/pipeline_editor.scss | 13 ++ .../concerns/dependency_proxy/group_access.rb | 4 - .../groups/dependency_proxies_controller.rb | 1 - .../burnup.iteration.query.graphql | 40 ++++++ .../burnup.milestone.query.graphql | 36 ++++++ .../burndown_chart/burnup.query.graphql | 75 ----------- .../dependency_proxy/group_setting_type.rb | 2 +- .../image_ttl_group_policy_type.rb | 2 +- app/policies/group_policy.rb | 2 +- .../registrations/welcome/show.html.haml | 2 +- app/workers/deployments/hooks_worker.rb | 3 + config/routes/user.rb | 2 +- doc/ci/cloud_services/index.md | 3 + .../index.md | 45 ++----- doc/ci/secrets/index.md | 12 +- doc/ci/variables/predefined_variables.md | 2 +- .../dependency_scanning/index.md | 14 +- .../reduce_dependency_proxy_storage.md | 2 + doc/user/permissions.md | 3 +- package.json | 2 +- .../file-nav/pipeline_editor_file_nav_spec.js | 19 ++- .../components/file-tree/constants.js | 1 - .../components/file-tree/container_spec.js | 121 ++++++++++++++---- .../components/file-tree/file_item_spec.js | 25 +++- spec/frontend/pipeline_editor/mock_data.js | 34 +++++ .../group_settings/update_spec.rb | 2 +- .../image_ttl_group_policy/update_spec.rb | 4 +- .../group_setting_type_spec.rb | 6 + .../image_ttl_group_policy_type_spec.rb | 2 +- spec/policies/group_policy_spec.rb | 9 ++ .../dependency_proxy_group_setting_spec.rb | 12 +- .../dependency_proxy_image_ttl_policy_spec.rb | 12 +- .../group_settings/update_spec.rb | 2 +- .../image_ttl_group_policy/update_spec.rb | 2 +- .../group_settings/update_service_spec.rb | 2 +- .../update_service_spec.rb | 6 +- spec/workers/deployments/hooks_worker_spec.rb | 10 ++ .../cluster_management.tar.gz | Bin 17708 -> 14705 bytes yarn.lock | 8 +- 47 files changed, 504 insertions(+), 205 deletions(-) create mode 100644 .rubocop_todo/style/string_literals_in_interpolation.yml create mode 100644 app/graphql/queries/burndown_chart/burnup.iteration.query.graphql create mode 100644 app/graphql/queries/burndown_chart/burnup.milestone.query.graphql delete mode 100644 app/graphql/queries/burndown_chart/burnup.query.graphql delete mode 100644 spec/frontend/pipeline_editor/components/file-tree/constants.js diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index d0e08e04927..3f495ef85b2 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -242,10 +242,3 @@ Style/SingleArgumentDig: # Cop supports --auto-correct. Style/StringConcatenation: Enabled: false - -# Offense count: 109 -# Cop supports --auto-correct. -# Configuration parameters: EnforcedStyle. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiteralsInInterpolation: - Enabled: false diff --git a/.rubocop_todo/style/string_literals_in_interpolation.yml b/.rubocop_todo/style/string_literals_in_interpolation.yml new file mode 100644 index 00000000000..3f3cb007306 --- /dev/null +++ b/.rubocop_todo/style/string_literals_in_interpolation.yml @@ -0,0 +1,69 @@ +--- +# Cop supports --auto-correct. +Style/StringLiteralsInInterpolation: + # Offense count: 119 + # Temporarily disabled due to too many offenses + Enabled: false + Exclude: + - 'app/graphql/mutations/base_mutation.rb' + - 'app/helpers/colors_helper.rb' + - 'app/helpers/todos_helper.rb' + - 'app/models/application_setting_implementation.rb' + - 'app/models/ci/namespace_mirror.rb' + - 'app/models/integrations/campfire.rb' + - 'app/models/integrations/jira.rb' + - 'app/models/serverless/domain.rb' + - 'app/services/draft_notes/publish_service.rb' + - 'app/services/projects/create_service.rb' + - 'app/validators/nested_attributes_duplicates_validator.rb' + - 'app/views/events/_event.atom.builder' + - 'app/workers/concerns/application_worker.rb' + - 'config/initializers/validate_database_config.rb' + - 'db/post_migrate/20210809123658_orphaned_invite_tokens_cleanup.rb' + - 'ee/app/helpers/ee/merge_requests_helper.rb' + - 'ee/app/models/license.rb' + - 'ee/app/services/epics/tree_reorder_service.rb' + - 'ee/lib/gitlab/elastic/helper.rb' + - 'ee/lib/pseudonymizer/pager.rb' + - 'ee/spec/features/admin/admin_settings_spec.rb' + - 'lib/api/helpers/snippets_helpers.rb' + - 'lib/api/validations/validators/check_assignees_count.rb' + - 'lib/banzai/filter/references/abstract_reference_filter.rb' + - 'lib/generators/gitlab/usage_metric_definition_generator.rb' + - 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb' + - 'lib/gitlab/ci/config/entry/job.rb' + - 'lib/gitlab/ci/yaml_processor.rb' + - 'lib/gitlab/database/partitioning_migration_helpers/table_management_helpers.rb' + - 'lib/gitlab/database/query_analyzers/prevent_cross_database_modification.rb' + - 'lib/gitlab/doctor/secrets.rb' + - 'lib/gitlab/endpoint_attributes/config.rb' + - 'lib/gitlab/graphql/queries.rb' + - 'lib/gitlab/quick_actions/extractor.rb' + - 'lib/gitlab/sanitizers/exif.rb' + - 'lib/gitlab/sidekiq_signals.rb' + - 'lib/gitlab/tracking/destinations/snowplow.rb' + - 'lib/tasks/gitlab/info.rake' + - 'lib/tasks/gitlab/sidekiq.rake' + - 'lib/tasks/gitlab/tw/codeowners.rake' + - 'qa/qa/ee/page/component/secure_report.rb' + - 'qa/qa/ee/page/group/secure/show.rb' + - 'qa/qa/resource/events/base.rb' + - 'qa/qa/service/cluster_provider/base.rb' + - 'qa/qa/service/cluster_provider/gcloud.rb' + - 'qa/qa/service/docker_run/gitlab_runner.rb' + - 'qa/qa/specs/helpers/context_selector.rb' + - 'qa/qa/tools/generate_perf_testdata.rb' + - 'rubocop/cop/migration/prevent_index_creation.rb' + - 'spec/controllers/projects/serverless/functions_controller_spec.rb' + - 'spec/features/commits_spec.rb' + - 'spec/features/dashboard/merge_requests_spec.rb' + - 'spec/features/users/login_spec.rb' + - 'spec/finders/serverless_domain_finder_spec.rb' + - 'spec/lib/banzai/filter/references/commit_reference_filter_spec.rb' + - 'spec/lib/banzai/filter/references/issue_reference_filter_spec.rb' + - 'spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb' + - 'spec/lib/gitlab/database/partitioning_migration_helpers/table_management_helpers_spec.rb' + - 'spec/lib/object_storage/direct_upload_spec.rb' + - 'spec/models/serverless/domain_spec.rb' + - 'spec/requests/api/keys_spec.rb' + - 'spec/support/database/prevent_cross_joins.rb' diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue index 998db653e0c..58df98d0fb7 100644 --- a/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue +++ b/app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue @@ -2,7 +2,7 @@ import { GlButton } from '@gitlab/ui'; import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { EDITOR_APP_STATUS_EMPTY } from '../../constants'; +import { EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_LOADING } from '../../constants'; import FileTreePopover from '../popovers/file_tree_popover.vue'; import BranchSwitcher from './branch_switcher.vue'; @@ -39,6 +39,9 @@ export default { }, }, computed: { + isAppLoading() { + return this.appStatus === EDITOR_APP_STATUS_LOADING; + }, showFileTreeToggle() { return ( this.glFeatures.pipelineEditorFileTree && @@ -62,6 +65,7 @@ export default { icon="file-tree" data-testid="file-tree-toggle" :aria-label="__('File Tree')" + :loading="isAppLoading" @click="onFileTreeBtnClick" /> diff --git a/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue b/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue index d1ff70ad518..8bffb281211 100644 --- a/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue +++ b/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue @@ -1,6 +1,8 @@ diff --git a/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue b/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue index d51a2874c9e..786d483b5b9 100644 --- a/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue +++ b/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue @@ -1,24 +1,45 @@ diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js index 0484da8641d..ff7c742f588 100644 --- a/app/assets/javascripts/pipeline_editor/constants.js +++ b/app/assets/javascripts/pipeline_editor/constants.js @@ -51,6 +51,7 @@ export const SOURCE_EDITOR_DEBOUNCE = 500; export const FILE_TREE_DISPLAY_KEY = 'pipeline_editor_file_tree_display'; export const FILE_TREE_POPOVER_DISMISSED_KEY = 'pipeline_editor_file_tree_popover_dismissed'; +export const FILE_TREE_TIP_DISMISSED_KEY = 'pipeline_editor_file_tree_tip_dismissed'; export const STARTER_TEMPLATE_NAME = 'Getting-Started'; diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql index df7de6a1f54..5354ed7c2d5 100644 --- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql +++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql @@ -3,6 +3,12 @@ query getCiConfigData($projectPath: ID!, $sha: String, $content: String!) { ciConfig(projectPath: $projectPath, sha: $sha, content: $content) { errors + includes { + location + type + blob + raw + } mergedYaml status stages { diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue index f5277fdbcca..59022a91322 100644 --- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue +++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue @@ -73,6 +73,9 @@ export default { showCommitForm() { return this.currentTab === CREATE_TAB; }, + includesFiles() { + return this.ciConfigData?.includes || []; + }, isFileTreeVisible() { return this.showFileTree && this.glFeatures.pipelineEditorFileTree; }, @@ -136,7 +139,11 @@ export default { v-on="$listeners" />
- +
div.gl-overflow-y-auto { + max-height: 220px; + + @media (min-width: $breakpoint-md) { + max-height: 700px; + } +} + +.file-tree-includes-link:hover > svg { + @include gl-display-block; + top: 2px; +} diff --git a/app/controllers/concerns/dependency_proxy/group_access.rb b/app/controllers/concerns/dependency_proxy/group_access.rb index 44611641529..45392625e45 100644 --- a/app/controllers/concerns/dependency_proxy/group_access.rb +++ b/app/controllers/concerns/dependency_proxy/group_access.rb @@ -18,9 +18,5 @@ module DependencyProxy def authorize_read_dependency_proxy! access_denied! unless can?(auth_user, :read_dependency_proxy, group) end - - def authorize_admin_dependency_proxy! - access_denied! unless can?(auth_user, :admin_dependency_proxy, group) - end end end diff --git a/app/controllers/groups/dependency_proxies_controller.rb b/app/controllers/groups/dependency_proxies_controller.rb index afa2af0d173..8e134529c34 100644 --- a/app/controllers/groups/dependency_proxies_controller.rb +++ b/app/controllers/groups/dependency_proxies_controller.rb @@ -4,7 +4,6 @@ module Groups class DependencyProxiesController < Groups::ApplicationController include ::DependencyProxy::GroupAccess - before_action :authorize_admin_dependency_proxy!, only: :update before_action :verify_dependency_proxy_enabled! feature_category :dependency_proxy diff --git a/app/graphql/queries/burndown_chart/burnup.iteration.query.graphql b/app/graphql/queries/burndown_chart/burnup.iteration.query.graphql new file mode 100644 index 00000000000..ff50c34ade3 --- /dev/null +++ b/app/graphql/queries/burndown_chart/burnup.iteration.query.graphql @@ -0,0 +1,40 @@ +query BurnupTimesSeriesIterationData( + $iterationId: IterationID! + $weight: Boolean = false + $fullPath: String +) { + iteration(id: $iterationId) { + __typename + id + title + report(fullPath: $fullPath) { + __typename + burnupTimeSeries { + __typename + date + completedCount @skip(if: $weight) + scopeCount @skip(if: $weight) + completedWeight @include(if: $weight) + scopeWeight @include(if: $weight) + } + stats { + __typename + total { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + complete { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + incomplete { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + } + } + } +} diff --git a/app/graphql/queries/burndown_chart/burnup.milestone.query.graphql b/app/graphql/queries/burndown_chart/burnup.milestone.query.graphql new file mode 100644 index 00000000000..18e59249500 --- /dev/null +++ b/app/graphql/queries/burndown_chart/burnup.milestone.query.graphql @@ -0,0 +1,36 @@ +query BurnupTimesSeriesMilestoneData($milestoneId: MilestoneID!, $weight: Boolean = false) { + milestone(id: $milestoneId) { + __typename + id + title + report { + __typename + burnupTimeSeries { + __typename + date + completedCount @skip(if: $weight) + scopeCount @skip(if: $weight) + completedWeight @include(if: $weight) + scopeWeight @include(if: $weight) + } + stats { + __typename + total { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + complete { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + incomplete { + __typename + count @skip(if: $weight) + weight @include(if: $weight) + } + } + } + } +} diff --git a/app/graphql/queries/burndown_chart/burnup.query.graphql b/app/graphql/queries/burndown_chart/burnup.query.graphql deleted file mode 100644 index 0795645f8b7..00000000000 --- a/app/graphql/queries/burndown_chart/burnup.query.graphql +++ /dev/null @@ -1,75 +0,0 @@ -query BurnupTimesSeriesData( - $id: ID! - $isIteration: Boolean = false - $weight: Boolean = false - $fullPath: String -) { - milestone(id: $id) @skip(if: $isIteration) { - __typename - id - title - report { - __typename - burnupTimeSeries { - __typename - date - completedCount @skip(if: $weight) - scopeCount @skip(if: $weight) - completedWeight @include(if: $weight) - scopeWeight @include(if: $weight) - } - stats { - __typename - total { - __typename - count @skip(if: $weight) - weight @include(if: $weight) - } - complete { - __typename - count @skip(if: $weight) - weight @include(if: $weight) - } - incomplete { - __typename - count @skip(if: $weight) - weight @include(if: $weight) - } - } - } - } - iteration(id: $id) @include(if: $isIteration) { - __typename - id - title - report(fullPath: $fullPath) { - __typename - burnupTimeSeries { - __typename - date - completedCount @skip(if: $weight) - scopeCount @skip(if: $weight) - completedWeight @include(if: $weight) - scopeWeight @include(if: $weight) - } - stats { - __typename - total { - __typename - count @skip(if: $weight) - weight @include(if: $weight) - } - complete { - __typename - count @skip(if: $weight) - weight @include(if: $weight) - } - incomplete { - __typename - count @skip(if: $weight) - weight @include(if: $weight) - } - } - } - } -} diff --git a/app/graphql/types/dependency_proxy/group_setting_type.rb b/app/graphql/types/dependency_proxy/group_setting_type.rb index 8b8b8572aa9..6c6f848d019 100644 --- a/app/graphql/types/dependency_proxy/group_setting_type.rb +++ b/app/graphql/types/dependency_proxy/group_setting_type.rb @@ -6,7 +6,7 @@ module Types description 'Group-level Dependency Proxy settings' - authorize :read_dependency_proxy + authorize :admin_dependency_proxy field :enabled, GraphQL::Types::Boolean, null: false, description: 'Indicates whether the dependency proxy is enabled for the group.' end diff --git a/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb b/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb index 9ab7c50998d..b8c178539a0 100644 --- a/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb +++ b/app/graphql/types/dependency_proxy/image_ttl_group_policy_type.rb @@ -6,7 +6,7 @@ module Types description 'Group-level Dependency Proxy TTL policy settings' - authorize :read_dependency_proxy + authorize :admin_dependency_proxy field :created_at, Types::TimeType, null: true, description: 'Timestamp of creation.' field :enabled, GraphQL::Types::Boolean, null: false, description: 'Indicates whether the policy is enabled or disabled.' diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index f6932d30ce2..d397d77bc1a 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -251,7 +251,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy rule { dependency_proxy_access_allowed & dependency_proxy_available } .enable :read_dependency_proxy - rule { developer & dependency_proxy_available }.policy do + rule { maintainer & dependency_proxy_available }.policy do enable :admin_dependency_proxy end diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml index 1f6976d4f38..62499f1a6b6 100644 --- a/app/views/registrations/welcome/show.html.haml +++ b/app/views/registrations/welcome/show.html.haml @@ -17,7 +17,7 @@ %p.gl-text-center= html_escape(_('%{gitlab_experience_text}. We won\'t share this information with anyone.')) % { gitlab_experience_text: gitlab_experience_text } - else %p.gl-text-center= html_escape(_('%{gitlab_experience_text}. Don\'t worry, this information isn\'t shared outside of your self-managed GitLab instance.')) % { gitlab_experience_text: gitlab_experience_text } - = gitlab_ui_form_for(current_user, url: users_sign_up_welcome_path, html: { class: 'card gl-w-full! gl-p-5', 'aria-live' => 'assertive' }) do |f| + = gitlab_ui_form_for(current_user, url: users_sign_up_welcome_path, html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome', 'aria-live' => 'assertive' }) do |f| .devise-errors = render 'devise/shared/error_messages', resource: current_user .row diff --git a/app/workers/deployments/hooks_worker.rb b/app/workers/deployments/hooks_worker.rb index 31c57e5c001..608601b4eb9 100644 --- a/app/workers/deployments/hooks_worker.rb +++ b/app/workers/deployments/hooks_worker.rb @@ -13,6 +13,9 @@ module Deployments params = params.with_indifferent_access if (deploy = Deployment.find_by_id(params[:deployment_id])) + log_extra_metadata_on_done(:deployment_project_id, deploy.project.id) + log_extra_metadata_on_done(:deployment_id, params[:deployment_id]) + deploy.execute_hooks(params[:status_changed_at].to_time) end end diff --git a/config/routes/user.rb b/config/routes/user.rb index 64dc56e18ec..ccacf817cc5 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -27,7 +27,7 @@ devise_controllers = { omniauth_callbacks: :omniauth_callbacks, sessions: :sessions, confirmations: :confirmations } -if ::Gitlab.ee? && ::Gitlab::Geo.connected? && ::Gitlab::Geo.secondary? +if ::Gitlab.ee? && ::Gitlab::Geo.secondary?(infer_without_database: true) devise_for :users, controllers: devise_controllers, path_names: { sign_in: 'auth/geo/sign_in', sign_out: 'auth/geo/sign_out' } # When using Geo, the other type of routes should be present as well, as browsers diff --git a/doc/ci/cloud_services/index.md b/doc/ci/cloud_services/index.md index 0f404927694..1493a930099 100644 --- a/doc/ci/cloud_services/index.md +++ b/doc/ci/cloud_services/index.md @@ -25,6 +25,9 @@ review for the pipeline, focusing on the additional access. You can use the [sof as a starting point, and for more information about supply chain attacks, see [How a DevOps Platform helps protect against supply chain attacks](https://about.gitlab.com/blog/2021/04/28/devops-platform-supply-chain-attacks/). +WARNING: +The `CI_JOB_JWT_V2` variable is under development [(alpha)](../../policy/alpha-beta-support.md#alpha-features) and is not yet suitable for production use. + ## Use cases - Removes the need to store secrets in your GitLab group or project. Temporary credentials can be retrieved from your cloud provider through OIDC. diff --git a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md index a2d8738bda3..389429f3f0f 100644 --- a/doc/ci/examples/authenticating-with-hashicorp-vault/index.md +++ b/doc/ci/examples/authenticating-with-hashicorp-vault/index.md @@ -22,14 +22,14 @@ This tutorial assumes you are familiar with GitLab CI/CD and Vault. To follow along, you must have: - An account on GitLab. -- Access to a running Vault server (at least v1.2.0) to configure authentication and to create roles and policies. You can use Open Source or Enterprise version of HashiCorp Vault. +- Access to a running Vault server (at least v1.2.0) to configure authentication and to create roles and policies. For HashiCorp Vaults, this can be the Open Source or Enterprise version. NOTE: You must replace the `vault.example.com` URL below with the URL of your Vault server, and `gitlab.example.com` with the URL of your GitLab instance. ## How it works -Each job has JSON Web Token (JWT) provided as CI/CD variable named `CI_JOB_JWT` or `CI_JOB_JWT_V2`. This JWT can be used to authenticate with Vault using the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt#jwt-authentication) method. +Each job has JSON Web Token (JWT) provided as CI/CD variable named `CI_JOB_JWT`. This JWT can be used to authenticate with Vault using the [JWT Auth](https://www.vaultproject.io/docs/auth/jwt#jwt-authentication) method. The following fields are included in the JWT: @@ -40,8 +40,7 @@ The following fields are included in the JWT: | `iat` | Always | Issued at | | `nbf` | Always | Not valid before | | `exp` | Always | Expires at | -| `sub` | Always | `project_path:{group}/{project}:ref_type:{type}:ref:{branch_name}` | -| `aud` | Always | Issuer, the domain of your GitLab instance | +| `sub` | Always | Subject (job ID) | | `namespace_id` | Always | Use this to scope to group or user level namespace by ID | | `namespace_path` | Always | Use this to scope to group or user level namespace by path | | `project_id` | Always | Use this to scope to project by ID | @@ -63,12 +62,11 @@ Example JWT payload: ```json { "jti": "c82eeb0c-5c6f-4a33-abf5-4c474b92b558", - "iss": "https://gitlab.example.com", - "aud": "https://gitlab.example.com", + "iss": "gitlab.example.com", "iat": 1585710286, "nbf": 1585798372, "exp": 1585713886, - "sub": "project_path:mygroup/myproject:ref_type:branch:ref:main", + "sub": "job_1212", "namespace_id": "1", "namespace_path": "mygroup", "project_id": "22", @@ -93,8 +91,6 @@ You can use this JWT and your instance's JWKS endpoint (`https://gitlab.example. When configuring roles in Vault, you can use [bound_claims](https://www.vaultproject.io/docs/auth/jwt#bound-claims) to match against the JWT's claims and restrict which secrets each CI job has access to. -You must use `bound_audiences` to match against the JWT's audience claim. - To communicate with Vault, you can use either its CLI client or perform API requests (using `curl` or another client). ## Example @@ -158,8 +154,7 @@ $ vault write auth/jwt/role/myproject-staging - < [Required permissions](https://gitlab.com/gitlab-org/gitlab/-/issues/350682) changed from developer to maintainer in GitLab 15.0. + ### Enable cleanup policies from within GitLab > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340777) in GitLab 14.6 diff --git a/doc/user/permissions.md b/doc/user/permissions.md index f2077cc92ca..56474796b8c 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -400,8 +400,9 @@ The following table lists group permissions available for each role: | Create/edit/delete group milestones | | ✓ | ✓ | ✓ | ✓ | | Create/edit/delete iterations | | ✓ | ✓ | ✓ | ✓ | | Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ | -| Enable/disable a dependency proxy | | | ✓ | ✓ | ✓ | +| Enable/disable a dependency proxy | | | | ✓ | ✓ | | Purge the dependency proxy for a group | | | | | ✓ | +| Create/edit/delete dependency proxy [cleanup policies](packages/dependency_proxy/reduce_dependency_proxy_storage.md#cleanup-policies) | | | | ✓ | ✓ | | Use [security dashboard](application_security/security_dashboard/index.md) | | | ✓ | ✓ | ✓ | | View group Audit Events | | | ✓ (7) | ✓ (7) | ✓ | | Create subgroup | | | | ✓ (1) | ✓ | diff --git a/package.json b/package.json index c99770122fc..f5642506772 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "codesandbox-api": "0.0.23", "compression-webpack-plugin": "^5.0.2", "copy-webpack-plugin": "^6.4.1", - "core-js": "^3.22.4", + "core-js": "^3.22.5", "cron-validator": "^1.1.1", "cronstrue": "^1.122.0", "cropper": "^2.3.0", diff --git a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js b/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js index cbd9c86aabd..a61796dbed2 100644 --- a/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js +++ b/spec/frontend/pipeline_editor/components/file-nav/pipeline_editor_file_nav_spec.js @@ -7,7 +7,11 @@ import BranchSwitcher from '~/pipeline_editor/components/file_nav/branch_switche import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue'; import FileTreePopover from '~/pipeline_editor/components/popovers/file_tree_popover.vue'; import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql'; -import { EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_VALID } from '~/pipeline_editor/constants'; +import { + EDITOR_APP_STATUS_EMPTY, + EDITOR_APP_STATUS_LOADING, + EDITOR_APP_STATUS_VALID, +} from '~/pipeline_editor/constants'; Vue.use(VueApollo); @@ -109,6 +113,19 @@ describe('Pipeline editor file nav', () => { }); }); + describe('when app is in a global loading state', () => { + it('renders the file tree button with a loading icon', () => { + createComponent({ + appStatus: EDITOR_APP_STATUS_LOADING, + isNewCiConfigFile: false, + pipelineEditorFileTree: true, + }); + + expect(findFileTreeBtn().exists()).toBe(true); + expect(findFileTreeBtn().attributes('loading')).toBe('true'); + }); + }); + describe('when editor has a non-empty config file open', () => { beforeEach(() => { createComponent({ diff --git a/spec/frontend/pipeline_editor/components/file-tree/constants.js b/spec/frontend/pipeline_editor/components/file-tree/constants.js deleted file mode 100644 index 2f0ea978c2b..00000000000 --- a/spec/frontend/pipeline_editor/components/file-tree/constants.js +++ /dev/null @@ -1 +0,0 @@ -export const MOCK_DEFAULT_CI_FILE = '.gitlab-ci.yml'; diff --git a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js b/spec/frontend/pipeline_editor/components/file-tree/container_spec.js index e449a7e5753..615a3eaac47 100644 --- a/spec/frontend/pipeline_editor/components/file-tree/container_spec.js +++ b/spec/frontend/pipeline_editor/components/file-tree/container_spec.js @@ -1,26 +1,39 @@ import { nextTick } from 'vue'; import { shallowMount } from '@vue/test-utils'; import { GlAlert } from '@gitlab/ui'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import { createMockDirective } from 'helpers/vue_mock_directive'; import PipelineEditorFileTreeContainer from '~/pipeline_editor/components/file_tree/container.vue'; import PipelineEditorFileTreeItem from '~/pipeline_editor/components/file_tree/file_item.vue'; -import { MOCK_DEFAULT_CI_FILE } from './constants'; +import { FILE_TREE_TIP_DISMISSED_KEY } from '~/pipeline_editor/constants'; +import { mockCiConfigPath, mockIncludes } from '../../mock_data'; describe('Pipeline editor file nav', () => { let wrapper; - const createComponent = ({ stubs } = {}) => { - wrapper = shallowMount(PipelineEditorFileTreeContainer, { - provide: { - ciConfigPath: MOCK_DEFAULT_CI_FILE, - }, - stubs, - }); + const createComponent = ({ includes = mockIncludes, stubs } = {}) => { + wrapper = extendedWrapper( + shallowMount(PipelineEditorFileTreeContainer, { + provide: { + ciConfigPath: mockCiConfigPath, + }, + propsData: { + includes, + }, + directives: { + GlTooltip: createMockDirective(), + }, + stubs, + }), + ); }; const findTip = () => wrapper.findComponent(GlAlert); - const fileTreeItem = () => wrapper.findComponent(PipelineEditorFileTreeItem); + const findCurrentConfigFilename = () => wrapper.findByTestId('current-config-filename'); + const fileTreeItems = () => wrapper.findAll(PipelineEditorFileTreeItem); afterEach(() => { + localStorage.clear(); wrapper.destroy(); }); @@ -30,27 +43,91 @@ describe('Pipeline editor file nav', () => { }); it('renders config file as a file item', () => { - expect(fileTreeItem().exists()).toBe(true); - expect(fileTreeItem().props('fileName')).toBe(MOCK_DEFAULT_CI_FILE); - }); - - it('renders tip', () => { - expect(findTip().exists()).toBe(true); + expect(findCurrentConfigFilename().text()).toBe(mockCiConfigPath); }); }); - describe('alert tip', () => { + describe('when includes list is empty', () => { + describe('when dismiss state is not saved in local storage', () => { + beforeEach(() => { + createComponent({ + includes: [], + stubs: { GlAlert }, + }); + }); + + it('does not render filenames', () => { + expect(fileTreeItems().exists()).toBe(false); + }); + + it('renders alert tip', async () => { + expect(findTip().exists()).toBe(true); + }); + + it('can dismiss the tip', async () => { + expect(findTip().exists()).toBe(true); + + findTip().vm.$emit('dismiss'); + await nextTick(); + + expect(findTip().exists()).toBe(false); + }); + }); + + describe('when dismiss state is saved in local storage', () => { + beforeEach(() => { + localStorage.setItem(FILE_TREE_TIP_DISMISSED_KEY, 'true'); + createComponent({ + includes: [], + stubs: { GlAlert }, + }); + }); + + it('does not render alert tip', async () => { + expect(findTip().exists()).toBe(false); + }); + }); + + describe('when component receives new props with includes files', () => { + beforeEach(() => { + createComponent({ includes: [] }); + }); + + it('hides tip and renders list of files', async () => { + expect(findTip().exists()).toBe(true); + expect(fileTreeItems()).toHaveLength(0); + + await wrapper.setProps({ includes: mockIncludes }); + + expect(findTip().exists()).toBe(false); + expect(fileTreeItems()).toHaveLength(mockIncludes.length); + }); + }); + }); + + describe('when there are includes files', () => { beforeEach(() => { createComponent({ stubs: { GlAlert } }); }); - it('can dismiss the tip', async () => { - expect(findTip().exists()).toBe(true); - - findTip().vm.$emit('dismiss'); - await nextTick(); - + it('does not render alert tip', () => { expect(findTip().exists()).toBe(false); }); + + it('renders the list of files', () => { + expect(fileTreeItems()).toHaveLength(mockIncludes.length); + }); + + describe('when component receives new props with empty includes', () => { + it('shows tip and does not render list of files', async () => { + expect(findTip().exists()).toBe(false); + expect(fileTreeItems()).toHaveLength(mockIncludes.length); + + await wrapper.setProps({ includes: [] }); + + expect(findTip().exists()).toBe(true); + expect(fileTreeItems()).toHaveLength(0); + }); + }); }); }); diff --git a/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js b/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js index ec496b01f9a..f12ac14c6be 100644 --- a/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js +++ b/spec/frontend/pipeline_editor/components/file-tree/file_item_spec.js @@ -1,20 +1,22 @@ +import { GlLink } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import PipelineEditorFileTreeItem from '~/pipeline_editor/components/file_tree/file_item.vue'; -import { MOCK_DEFAULT_CI_FILE } from './constants'; +import { mockIncludesWithBlob, mockDefaultIncludes } from '../../mock_data'; describe('Pipeline editor file nav', () => { let wrapper; - const createComponent = () => { + const createComponent = ({ file = mockDefaultIncludes } = {}) => { wrapper = shallowMount(PipelineEditorFileTreeItem, { propsData: { - fileName: MOCK_DEFAULT_CI_FILE, + file, }, }); }; const fileIcon = () => wrapper.findComponent(FileIcon); + const link = () => wrapper.findComponent(GlLink); afterEach(() => { wrapper.destroy(); @@ -27,11 +29,24 @@ describe('Pipeline editor file nav', () => { it('renders file icon', () => { expect(fileIcon().exists()).toBe(true); - expect(fileIcon().props('fileName')).toBe(MOCK_DEFAULT_CI_FILE); }); it('renders file name', () => { - expect(wrapper.text()).toBe(MOCK_DEFAULT_CI_FILE); + expect(wrapper.text()).toBe(mockDefaultIncludes.location); + }); + + it('links to raw path by default', () => { + expect(link().attributes('href')).toBe(mockDefaultIncludes.raw); + }); + }); + + describe('when file has blob link', () => { + beforeEach(() => { + createComponent({ file: mockIncludesWithBlob }); + }); + + it('links to blob path', () => { + expect(link().attributes('href')).toBe(mockIncludesWithBlob.blob); }); }); }); diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js index f02f6870653..748519dfbae 100644 --- a/spec/frontend/pipeline_editor/mock_data.js +++ b/spec/frontend/pipeline_editor/mock_data.js @@ -82,12 +82,46 @@ const mockJobFields = { __typename: 'CiConfigJob', }; +export const mockIncludesWithBlob = { + location: 'test-include.yml', + type: 'local', + blob: + 'http://gdk.test:3000/root/upstream/-/blob/dd54f00bb3645f8ddce7665d2ffb3864540399cb/test-include.yml', + raw: + 'http://gdk.test:3000/root/upstream/-/raw/dd54f00bb3645f8ddce7665d2ffb3864540399cb/test-include.yml', + __typename: 'CiConfigInclude', +}; + +export const mockDefaultIncludes = { + location: 'npm.gitlab-ci.yml', + type: 'template', + blob: null, + raw: + 'https://gitlab.com/gitlab-org/gitlab/-/raw/master/lib/gitlab/ci/templates/npm.gitlab-ci.yml', + __typename: 'CiConfigInclude', +}; + +export const mockIncludes = [ + mockDefaultIncludes, + mockIncludesWithBlob, + { + location: 'a_really_really_long_name_for_includes_file.yml', + type: 'local', + blob: + 'http://gdk.test:3000/root/upstream/-/blob/dd54f00bb3645f8ddce7665d2ffb3864540399cb/a_really_really_long_name_for_includes_file.yml', + raw: + 'http://gdk.test:3000/root/upstream/-/raw/dd54f00bb3645f8ddce7665d2ffb3864540399cb/a_really_really_long_name_for_includes_file.yml', + __typename: 'CiConfigInclude', + }, +]; + // Mock result of the graphql query at: // app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql export const mockCiConfigQueryResponse = { data: { ciConfig: { errors: [], + includes: mockIncludes, mergedYaml: mockCiYml, status: CI_CONFIG_STATUS_VALID, stages: { diff --git a/spec/graphql/mutations/dependency_proxy/group_settings/update_spec.rb b/spec/graphql/mutations/dependency_proxy/group_settings/update_spec.rb index 35d3224d5ba..ae368e4d37e 100644 --- a/spec/graphql/mutations/dependency_proxy/group_settings/update_spec.rb +++ b/spec/graphql/mutations/dependency_proxy/group_settings/update_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Mutations::DependencyProxy::GroupSettings::Update do where(:user_role, :shared_examples_name) do :maintainer | 'updating the dependency proxy group settings' - :developer | 'updating the dependency proxy group settings' + :developer | 'denying access to dependency proxy group settings' :reporter | 'denying access to dependency proxy group settings' :guest | 'denying access to dependency proxy group settings' :anonymous | 'denying access to dependency proxy group settings' diff --git a/spec/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb b/spec/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb index 792e87f0d25..1e5059d7ef7 100644 --- a/spec/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb +++ b/spec/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb @@ -72,7 +72,7 @@ RSpec.describe Mutations::DependencyProxy::ImageTtlGroupPolicy::Update do where(:user_role, :shared_examples_name) do :maintainer | 'updating the dependency proxy image ttl policy' - :developer | 'updating the dependency proxy image ttl policy' + :developer | 'denying access to dependency proxy image ttl policy' :reporter | 'denying access to dependency proxy image ttl policy' :guest | 'denying access to dependency proxy image ttl policy' :anonymous | 'denying access to dependency proxy image ttl policy' @@ -92,7 +92,7 @@ RSpec.describe Mutations::DependencyProxy::ImageTtlGroupPolicy::Update do where(:user_role, :shared_examples_name) do :maintainer | 'creating the dependency proxy image ttl policy' - :developer | 'creating the dependency proxy image ttl policy' + :developer | 'denying access to dependency proxy image ttl policy' :reporter | 'denying access to dependency proxy image ttl policy' :guest | 'denying access to dependency proxy image ttl policy' :anonymous | 'denying access to dependency proxy image ttl policy' diff --git a/spec/graphql/types/dependency_proxy/group_setting_type_spec.rb b/spec/graphql/types/dependency_proxy/group_setting_type_spec.rb index 7c6d7b8aece..cd648cf4b4d 100644 --- a/spec/graphql/types/dependency_proxy/group_setting_type_spec.rb +++ b/spec/graphql/types/dependency_proxy/group_setting_type_spec.rb @@ -10,4 +10,10 @@ RSpec.describe GitlabSchema.types['DependencyProxySetting'] do expect(described_class).to include_graphql_fields(*expected_fields) end + + it { expect(described_class).to require_graphql_authorizations(:admin_dependency_proxy) } + + it { expect(described_class.graphql_name).to eq('DependencyProxySetting') } + + it { expect(described_class.description).to eq('Group-level Dependency Proxy settings') } end diff --git a/spec/graphql/types/dependency_proxy/image_ttl_group_policy_type_spec.rb b/spec/graphql/types/dependency_proxy/image_ttl_group_policy_type_spec.rb index 46347e0434f..af0f91a844e 100644 --- a/spec/graphql/types/dependency_proxy/image_ttl_group_policy_type_spec.rb +++ b/spec/graphql/types/dependency_proxy/image_ttl_group_policy_type_spec.rb @@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['DependencyProxyImageTtlGroupPolicy'] do it { expect(described_class.description).to eq('Group-level Dependency Proxy TTL policy settings') } - it { expect(described_class).to require_graphql_authorizations(:read_dependency_proxy) } + it { expect(described_class).to require_graphql_authorizations(:admin_dependency_proxy) } it 'includes dependency proxy image ttl policy fields' do expected_fields = %w[enabled ttl created_at updated_at] diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index e8d3f165fb9..d64b59282a2 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -914,12 +914,21 @@ RSpec.describe GroupPolicy do context 'reporter' do let(:current_user) { reporter } + it { is_expected.to be_allowed(:read_dependency_proxy) } it { is_expected.to be_disallowed(:admin_dependency_proxy) } end context 'developer' do let(:current_user) { developer } + it { is_expected.to be_allowed(:read_dependency_proxy) } + it { is_expected.to be_disallowed(:admin_dependency_proxy) } + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it { is_expected.to be_allowed(:read_dependency_proxy) } it { is_expected.to be_allowed(:admin_dependency_proxy) } end end diff --git a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb index de3dbc5c324..d21c3046c1a 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_group_setting_spec.rb @@ -47,14 +47,14 @@ RSpec.describe 'getting dependency proxy settings for a group' do context 'with different permissions' do where(:group_visibility, :role, :access_granted) do :private | :maintainer | true - :private | :developer | true - :private | :reporter | true - :private | :guest | true + :private | :developer | false + :private | :reporter | false + :private | :guest | false :private | :anonymous | false :public | :maintainer | true - :public | :developer | true - :public | :reporter | true - :public | :guest | true + :public | :developer | false + :public | :reporter | false + :public | :guest | false :public | :anonymous | false end diff --git a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb index c8797d84906..40f4b082072 100644 --- a/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb +++ b/spec/requests/api/graphql/group/dependency_proxy_image_ttl_policy_spec.rb @@ -46,14 +46,14 @@ RSpec.describe 'getting dependency proxy image ttl policy for a group' do context 'with different permissions' do where(:group_visibility, :role, :access_granted) do :private | :maintainer | true - :private | :developer | true - :private | :reporter | true - :private | :guest | true + :private | :developer | false + :private | :reporter | false + :private | :guest | false :private | :anonymous | false :public | :maintainer | true - :public | :developer | true - :public | :reporter | true - :public | :guest | true + :public | :developer | false + :public | :reporter | false + :public | :guest | false :public | :anonymous | false end diff --git a/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb b/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb index f05bf23ad27..9eb13e534ac 100644 --- a/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb +++ b/spec/requests/api/graphql/mutations/dependency_proxy/group_settings/update_spec.rb @@ -50,7 +50,7 @@ RSpec.describe 'Updating the dependency proxy group settings' do context 'with permission' do before do - group.add_developer(user) + group.add_maintainer(user) end it 'returns the updated dependency proxy settings', :aggregate_failures do diff --git a/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb b/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb index c9e9a22ee0b..31ba7ecdf0e 100644 --- a/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb +++ b/spec/requests/api/graphql/mutations/dependency_proxy/image_ttl_group_policy/update_spec.rb @@ -52,7 +52,7 @@ RSpec.describe 'Updating the dependency proxy image ttl policy' do context 'with permission' do before do - group.add_developer(user) + group.add_maintainer(user) end it 'returns the updated dependency proxy image ttl policy', :aggregate_failures do diff --git a/spec/services/dependency_proxy/group_settings/update_service_spec.rb b/spec/services/dependency_proxy/group_settings/update_service_spec.rb index 6f8c55daa8d..4954d9ec267 100644 --- a/spec/services/dependency_proxy/group_settings/update_service_spec.rb +++ b/spec/services/dependency_proxy/group_settings/update_service_spec.rb @@ -42,7 +42,7 @@ RSpec.describe ::DependencyProxy::GroupSettings::UpdateService do where(:user_role, :shared_examples_name) do :maintainer | 'updating the dependency proxy group settings' - :developer | 'updating the dependency proxy group settings' + :developer | 'denying access to dependency proxy group settings' :reporter | 'denying access to dependency proxy group settings' :guest | 'denying access to dependency proxy group settings' :anonymous | 'denying access to dependency proxy group settings' diff --git a/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb b/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb index ceac8985c8e..3a6ba2cca71 100644 --- a/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb +++ b/spec/services/dependency_proxy/image_ttl_group_policies/update_service_spec.rb @@ -72,7 +72,7 @@ RSpec.describe ::DependencyProxy::ImageTtlGroupPolicies::UpdateService do where(:user_role, :shared_examples_name) do :maintainer | 'updating the dependency proxy image ttl policy' - :developer | 'updating the dependency proxy image ttl policy' + :developer | 'denying access to dependency proxy image ttl policy' :reporter | 'denying access to dependency proxy image ttl policy' :guest | 'denying access to dependency proxy image ttl policy' :anonymous | 'denying access to dependency proxy image ttl policy' @@ -92,7 +92,7 @@ RSpec.describe ::DependencyProxy::ImageTtlGroupPolicies::UpdateService do where(:user_role, :shared_examples_name) do :maintainer | 'creating the dependency proxy image ttl policy' - :developer | 'creating the dependency proxy image ttl policy' + :developer | 'denying access to dependency proxy image ttl policy' :reporter | 'denying access to dependency proxy image ttl policy' :guest | 'denying access to dependency proxy image ttl policy' :anonymous | 'denying access to dependency proxy image ttl policy' @@ -108,7 +108,7 @@ RSpec.describe ::DependencyProxy::ImageTtlGroupPolicies::UpdateService do context 'when the policy is not found' do before do - group.add_developer(user) + group.add_maintainer(user) expect(group).to receive(:dependency_proxy_image_ttl_policy).and_return nil end diff --git a/spec/workers/deployments/hooks_worker_spec.rb b/spec/workers/deployments/hooks_worker_spec.rb index 29b3e8d3ee4..a9240b45360 100644 --- a/spec/workers/deployments/hooks_worker_spec.rb +++ b/spec/workers/deployments/hooks_worker_spec.rb @@ -10,6 +10,16 @@ RSpec.describe Deployments::HooksWorker do allow(ProjectServiceWorker).to receive(:perform_async) end + it 'logs deployment and project IDs as metadata' do + deployment = create(:deployment, :running) + project = deployment.project + + expect(worker).to receive(:log_extra_metadata_on_done).with(:deployment_project_id, project.id) + expect(worker).to receive(:log_extra_metadata_on_done).with(:deployment_id, deployment.id) + + worker.perform(deployment_id: deployment.id, status_changed_at: Time.current) + end + it 'executes project services for deployment_hooks' do deployment = create(:deployment, :running) project = deployment.project diff --git a/vendor/project_templates/cluster_management.tar.gz b/vendor/project_templates/cluster_management.tar.gz index 843a8a355f16a52ba119b7da52bf4f0e27e2d8eb..97e18dc859b27f13ce99d649223380ed1f1fbde7 100644 GIT binary patch literal 14705 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 literal 17708 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 jG8(t3WIt_2DIGN*Ui& z{XCDfnNg&hMM93Th{T0B2}%>pk0WALS!C4>ox~^L{)SvICQ{>qlIjL;27@a8$7eP( zOCB`X+toLYhMq$EQ8~Li)S}h8F8R-H8%8G5Z&G|?I8fky8NY6?T>oT%(p&UjJj{m*Y_)a^ZJbG;e@+8F*v^5TTW*pLN2^3U0@i*YCy@KVsKJggOssI zduk&GdLwkTwbVNTI$KwNLE%g*_s!*aTK0cuLyJUeG<-{F`#=NWX;fpZ83{1d+xXuY z+en!qA5H*sitIKKFEaSW7j_P1+vQg53HR@eV3%dunzPC3&*C(^lrcPq`7lh)^2y(e zU3Rr>X+`$;;wMFFjF+`S{0JEx8!Nz*mzdJY*$K=1$)SNYPiEY2Gr&;0qvtf*m%ZxQ zLHm+QD2m~>fzCd93fdjlKgVuTha@*P@7V1S#Rjj0V(`4QOx6WG4ubp8%aVP~S*~cO zuQ==ZoT*kR{&c8Yx8IrcDftxuZEva8PTqJm>O*xDbf&J;La=>Kq5aa$1M zU6*vXaG<2@K@~SCkJY10h{}yvo#^LKo|v#VVQUhz-O*3U9KY(T$t%9q;otrjt>VO~ jCk}