Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-07 21:10:22 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-07 21:10:22 +0300
commit4d18bba787186aeb5bf8a0463fd145fae48b3234 (patch)
treedaa26f7daa8bf303ef0cfcc1bfe68d13eae74537
parent33c86930e0a657e1519082a9a00faae260a44882 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.dockerignore102
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/package-and-test/main.gitlab-ci.yml5
-rw-r--r--.gitlab/ci/qa-common/main.gitlab-ci.yml25
-rw-r--r--.gitlab/ci/qa-common/rules.gitlab-ci.yml4
-rw-r--r--.gitlab/ci/qa-common/variables.gitlab-ci.yml7
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml3
-rw-r--r--.gitlab/ci/rails/shared.gitlab-ci.yml2
-rw-r--r--.gitlab/ci/test-on-gdk/main.gitlab-ci.yml5
-rw-r--r--.gitlab/issue_templates/Design Sprint.md290
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue11
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue12
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue19
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue2
-rw-r--r--app/assets/javascripts/boards/graphql/group_boards.query.graphql8
-rw-r--r--app/assets/javascripts/boards/graphql/group_recent_boards.query.graphql8
-rw-r--r--app/assets/javascripts/boards/graphql/project_boards.query.graphql8
-rw-r--r--app/assets/javascripts/boards/graphql/project_recent_boards.query.graphql8
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue11
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue130
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue6
-rw-r--r--app/assets/javascripts/environments/constants.js36
-rw-r--r--app/assets/javascripts/environments/graphql/client.js17
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql14
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql14
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js49
-rw-r--r--app/assets/javascripts/environments/graphql/typedefs.graphql15
-rw-r--r--app/assets/javascripts/group_settings/components/shared_runners_form.vue81
-rw-r--r--app/assets/javascripts/group_settings/mount_shared_runners.js8
-rw-r--r--app/assets/javascripts/token_access/components/inbound_token_access.vue65
-rw-r--r--app/assets/javascripts/token_access/components/outbound_token_access.vue47
-rw-r--r--app/assets/javascripts/token_access/components/token_access_app.vue4
-rw-r--r--app/helpers/ci/runners_helper.rb19
-rw-r--r--app/models/ci/pipeline.rb1
-rw-r--r--app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb1
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--doc/api/pipeline_triggers.md12
-rw-r--r--doc/ci/triggers/index.md47
-rw-r--r--doc/ci/variables/predefined_variables.md2
-rw-r--r--doc/development/pipelines/internals.md106
-rw-r--r--doc/user/project/settings/project_access_tokens.md5
-rw-r--r--locale/gitlab.pot27
-rw-r--r--qa/Dockerfile2
-rw-r--r--qa/qa/specs/spec_helper.rb6
-rw-r--r--scripts/rspec_helpers.sh32
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js3
-rw-r--r--spec/frontend/boards/components/config_toggle_spec.js20
-rw-r--r--spec/frontend/boards/mock_data.js22
-rw-r--r--spec/frontend/environments/graphql/mock_data.js7
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js76
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js10
-rw-r--r--spec/frontend/environments/kubernetes_status_bar_spec.js142
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js6
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js51
-rw-r--r--spec/frontend/token_access/inbound_token_access_spec.js27
-rw-r--r--spec/frontend/token_access/outbound_token_access_spec.js6
-rw-r--r--spec/helpers/ci/runners_helper_spec.rb34
-rw-r--r--spec/models/ci/pipeline_spec.rb23
-rw-r--r--spec/models/project_team_spec.rb4
-rw-r--r--spec/support/fast_quarantine.rb7
61 files changed, 1273 insertions, 449 deletions
diff --git a/.dockerignore b/.dockerignore
index a2d54fa65dd..2ae1dadb020 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,82 +1,60 @@
# `build_from_dir` can't find Dockerfile when `.dockerignore` is "*"
# See https://github.com/swipely/docker-api/issues/484
-# Ignore all folders except the following files we need to build the QA image:
-# - ./config/initializers/0_inject_enterprise_edition_module.rb
-# - ./config/feature_flags
-# - ./ee/config/feature_flags
-# - ./ee/app/models/license.rb
-# - ./lib/gitlab_edition.rb
-# - ./lib/gitlab/utils.rb
-# - ./qa/
-# - ./INSTALLATION_TYPE
-# - ./VERSION
-
+# Ignore all folders except the one listed at the bottom of this file.
/.git/
/app/
/bin/
/builds/
/changelogs/
-/config/environments/
-/config/helpers/
-/config/knative/
-/config/locales/
-/config/prometheus/
-/config/routes/
+/config/
+/coverage/
/danger/
+/data/
/db/
/doc/
-/docker/
-/ee/bin/
-/ee/changelogs/
-/ee/config/events/
-/ee/config/metrics/
-/ee/config/routes/
-/ee/db/
-/ee/fixtures/
-/ee/lib/
-/ee/locale/
-/ee/spec/
+/ee/
+/file_hooks/
/fixtures/
-/templates/
-/lint/
-/lib/api/
-/lib/assets/
-/lib/backup/
-/lib/banzai/
-/lib/bitbucket/
-/lib/server/
-/lib/constraints/
-/lib/registry/
-/lib/policy/
-/lib/feature/
-/lib/generators/
-/lib/gitaly/
-/lib/api/
-/lib/token/
-/lib/mattermost/
-/lib/teams/
-/lib/storage/
-/lib/auth/
-/lib/peek/
-/lib/prometheus/
-/lib/quality/
-/lib/rouge/
-/lib/flaky/
-/lib/zip/
-/lib/sentry/
-/lib/serializers/
-/lib/support/
-/lib/check/
-/lib/tasks/
+/gems/
+/generator_templates/
+/glfm_specification/
+/haml_lint/
+/jh/
+/lib/
/locale/
/log/
-/modules/
+/metrics_server/
/node_modules/
-/plugins/
+/patches/
/public/
+/qa/
/rubocop/
/scripts/
/shared/
+/sidekiq_cluster/
/spec/
-/symbol/
+/storybook/
/tmp/
+/tooling/
+/vendor/
+/workhorse/
+
+!/config/initializers/0_inject_enterprise_edition_module.rb
+!/config/feature_flags/
+!/config/bundler_setup.rb
+!/lib/gitlab_edition.rb
+!/lib/gitlab/utils.rb
+!/spec/support/fast_quarantine.rb
+!/tooling/lib/tooling/fast_quarantine.rb
+!/INSTALLATION_TYPE
+!/VERSION
+!/gems/
+!/qa/
+!/vendor/gems/
+
+!/ee/app/models/license.rb
+!/ee/config/feature_flags/
+
+!/jh/qa/
+!/jh/lib/
+!/jh/config/feature_flags/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index bcca822fc0b..050f5f41515 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -163,7 +163,7 @@ variables:
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json
RSPEC_CHANGED_FILES_PATH: rspec/changed_files.txt
RSPEC_FAIL_FAST_THRESHOLD: 20
- RSPEC_FAST_QUARANTINE_LOCAL_PATH: rspec/fast_quarantine-gitlab.txt
+ RSPEC_FAST_QUARANTINE_PATH: rspec/fast_quarantine-gitlab.txt
RSPEC_FOSS_IMPACT_PIPELINE_TEMPLATE_YML: .gitlab/ci/rails/rspec-foss-impact.gitlab-ci.yml.erb
RSPEC_LAST_RUN_RESULTS_FILE: rspec/rspec_last_run_results.txt
RSPEC_MATCHING_JS_FILES_PATH: rspec/js_matching_files.txt
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
index 45e07ccf659..9d0958458cc 100644
--- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml
+++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml
@@ -35,6 +35,11 @@ download-knapsack-report:
- .download-knapsack-report
- .rules:download-knapsack
+download-fast-quarantine-report:
+ extends:
+ - .download-fast-quarantine-report
+ - .rules:download-fast-quarantine-report
+
cache-gems:
extends:
- .qa-install
diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml
index 603ac12c464..5c9043f8694 100644
--- a/.gitlab/ci/qa-common/main.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml
@@ -61,6 +61,11 @@ stages:
GITLAB_LICENSE_MODE: test
GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN
GITLAB_QA_OPTS: $EXTRA_GITLAB_QA_OPTS
+ before_script:
+ - !reference [.qa-base, before_script]
+ # Prepend the file paths with the absolute path from inside the container since the files will be read from there
+ - export RSPEC_FAST_QUARANTINE_PATH="/home/gitlab/qa/${RSPEC_FAST_QUARANTINE_PATH}"
+ - export RSPEC_SKIPPED_TESTS_REPORT_PATH="/home/gitlab/qa/rspec/skipped_tests-${CI_JOB_ID}.txt"
# Allow QA jobs to fail as they are flaky. The top level `package-and-e2e:ee`
# pipeline is not allowed to fail, so without allowing QA to fail, we will be
# blocking merges due to flaky tests.
@@ -85,6 +90,26 @@ stages:
- qa/knapsack/*.json
expire_in: 1 day
+.download-fast-quarantine-report:
+ image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}alpine:edge
+ stage: .pre
+ variables:
+ GIT_STRATEGY: none
+ before_script:
+ - apk add --no-cache --update curl bash
+ script:
+ - mkdir -p "${QA_RSPEC_REPORT_PATH}"
+ - |
+ if [[ ! -f "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}" ]]; then
+ curl --location -o "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}" "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/${RSPEC_FAST_QUARANTINE_PATH}" ||
+ echo "" > "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}"
+ fi
+ allow_failure: true
+ artifacts:
+ paths:
+ - "${QA_RSPEC_REPORT_PATH}/${RSPEC_FAST_QUARANTINE_FILE}"
+ expire_in: 1 day
+
.upload-knapsack-report:
extends:
- .generate-knapsack-report-base
diff --git a/.gitlab/ci/qa-common/rules.gitlab-ci.yml b/.gitlab/ci/qa-common/rules.gitlab-ci.yml
index b5963d24b81..7518f08398f 100644
--- a/.gitlab/ci/qa-common/rules.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/rules.gitlab-ci.yml
@@ -64,6 +64,10 @@
rules:
- when: always
+.rules:download-fast-quarantine-report:
+ rules:
+ - when: always
+
# ------------------------------------------
# Test
# ------------------------------------------
diff --git a/.gitlab/ci/qa-common/variables.gitlab-ci.yml b/.gitlab/ci/qa-common/variables.gitlab-ci.yml
index fe980293f3a..9498df47ecc 100644
--- a/.gitlab/ci/qa-common/variables.gitlab-ci.yml
+++ b/.gitlab/ci/qa-common/variables.gitlab-ci.yml
@@ -1,6 +1,7 @@
# Default variables for package-and-test
variables:
+ USE_OLD_RUBY_VERSION: "true"
REGISTRY_HOST: "registry.gitlab.com"
REGISTRY_GROUP: "gitlab-org"
ALLURE_JOB_NAME: $CI_PROJECT_NAME
@@ -11,4 +12,8 @@ variables:
# run all tests by default when package-and-test is included natively in other projects
# this will be overridden when selective test execution is used in gitlab canonical project
QA_RUN_ALL_TESTS: "true"
- USE_OLD_RUBY_VERSION: "true"
+ # Used by gitlab-qa to set up a volume for `${CI_PROJECT_DIR}/qa/rspec:/home/gitlab/qa/rspec/`
+ QA_RSPEC_REPORT_PATH: "${CI_PROJECT_DIR}/qa/rspec"
+ RSPEC_FAST_QUARANTINE_FILE: "fast_quarantine-gitlab.txt"
+ # This path is relative to /home/gitlab/qa/ in the QA container
+ RSPEC_FAST_QUARANTINE_PATH: "rspec/${RSPEC_FAST_QUARANTINE_FILE}"
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index 5b654627465..5aa0fabfadf 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -548,9 +548,6 @@ rspec:flaky-tests-report:
- .rails:rules:flaky-tests-report
stage: post-test
needs: !reference ["rspec:coverage", "needs"]
- variables:
- SKIPPED_TESTS_REPORT_PATH: rspec/skipped_tests_report.txt
- RETRIED_TESTS_REPORT_PATH: rspec/flaky/retried_tests_report.txt
before_script:
- source scripts/utils.sh
- source scripts/rspec_helpers.sh
diff --git a/.gitlab/ci/rails/shared.gitlab-ci.yml b/.gitlab/ci/rails/shared.gitlab-ci.yml
index 95f1210f05c..ce89e7ef689 100644
--- a/.gitlab/ci/rails/shared.gitlab-ci.yml
+++ b/.gitlab/ci/rails/shared.gitlab-ci.yml
@@ -25,6 +25,8 @@ include:
# gems could not be found under some circumstance. No idea why, hours wasted.
- run_timed_command "gem install knapsack --no-document"
- section_start "gitaly-test-spawn" "Spawning Gitaly"; scripts/gitaly-test-spawn; section_end "gitaly-test-spawn" # Do not use 'bundle exec' here
+ - export RSPEC_SKIPPED_TESTS_REPORT_PATH="rspec/skipped_tests-${CI_JOB_ID}.txt"
+ - export RSPEC_RETRIED_TESTS_REPORT_PATH="rspec/retried_tests-${CI_JOB_ID}.txt"
.no-redis-cluster:
variables:
diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
index a3e7900114b..41f85c492d9 100644
--- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
+++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
@@ -117,6 +117,11 @@ cache-gems:
gdk reconfigure &&\
gdk restart"
+download-fast-quarantine-report:
+ extends:
+ - .download-fast-quarantine-report
+ - .rules:download-fast-quarantine-report
+
gdk-qa-smoke:
extends:
- .gdk-qa-base
diff --git a/.gitlab/issue_templates/Design Sprint.md b/.gitlab/issue_templates/Design Sprint.md
index 9eaa9da3675..76f3ae1c869 100644
--- a/.gitlab/issue_templates/Design Sprint.md
+++ b/.gitlab/issue_templates/Design Sprint.md
@@ -2,18 +2,18 @@
This template outlines a sample set-up process, activities and deliverables for running a Remote Design Sprint. The specific activities and deliverables should be customized based on your objectives and timeline.
-Please refer to the [Remote Design Sprint Handbook page](#anchor-tag-to-handbook-page) for additional recommendations.
+Please refer to the [Remote Design Sprint Handbook page](https://about.gitlab.com/handbook/product/ux/design-sprint/) for additional recommendations.
## Design Sprint Focus
-* [ ] Have you [determined that a Design Sprint is appropriate for this project](#anchor-tag-to-handbook-page)?
-<!-- What is the focus of the [Design Sprint](https://about.gitlab.com/handbook/product/product-processes/#design-sprint)? What problem area will you be solving for and who is the target user? -->
+* [ ] Have you [determined that a Design Sprint is appropriate for this project](https://about.gitlab.com/handbook/product/ux/design-sprint/#when-to-opt-for-a-remote-design-sprint)?
+_What is the focus of the [Design Sprint](https://about.gitlab.com/handbook/product/product-processes/#design-sprint)? What problem area will you be solving for and who is the target user?_
## Objectives
-
-<!-- Try to describe the objectives of the Sprint in detail. eg "We want to introduce a new feature but we are unsure that we are thinking about the solution from the customer's perspective and through the Sprint we want to rethink the solution, prototype it and validate it with our customers" or "We are unhappy with the direction of one of our categories and we want to explore new directions with different stakeholders, reach to one solution and test it with users" or "Among the team we have different visions for a specific category and we want to work towards a solution we all support and test it with users". -->
+_What is the objective(s) this Design Sprint will entail?_
+<!-- Try to describe the objectives of the Sprint in detail. e.g., "We want to introduce a new feature but we are unsure that we are thinking about the solution from the customer's perspective, and through the Sprint we want to rethink the solution, prototype it and validate it with our customers" or "We are unhappy with the direction of one of our categories and we want to explore new directions with different stakeholders, reach to one solution and test it with users" or "Among the team we have different visions for a specific category and we want to work towards a solution we all support and test it with users". -->
## Outputs
-
+_Select which outputs you want to have at the end of the Sprint._
- [ ] A User testing flow.
- [ ] A Prototype to be tested with users.
- [ ] User testing analysis.
@@ -27,14 +27,7 @@ Please refer to the [Remote Design Sprint Handbook page](#anchor-tag-to-handbook
| YYYY-MM-DD | YYYY-MM-DD |
| TT:TT PST | TT:TT PST |
-
-### WHEN
-
-**Start date:**
-
-**End date:**
-
-**Reference time zone:**
+**Reference time zone:** All times will be posted in [UTC](https://www.timeanddate.com/worldclock/timezone/utc).
### WHERE
@@ -57,89 +50,52 @@ Please refer to the [Remote Design Sprint Handbook page](#anchor-tag-to-handbook
Here is the list of tools for the Sprint preparation, collaboration and documentation. Prior to the Sprint make sure you have access to all of the following:
* **GitLab**<br/>
-Each Sprint day outcomes and material will be documented in a separate issue under the Design Sprint epic.
-
-* **Mural** (You can join as anonymous but we need to be able to identify input against names, so please create an account beforehand.<br/>
+Each Sprint day outcomes and material will be documented in separate issues under the Design Sprint epic:
+ * **Kickoff:** (Kickoff Issue Link)
+ * **Day One:** (Day 1 Issue Link)
+ * **Day Two:** (Day 2 Issue Link)
+ * **Day Three:** (Day 3 Issue Link)
+ * **Day Four:** (Day 4 Issue Link)
+
+* **Mural** (You can join as anonymous but we need to be able to identify input against names, so please create an account beforehand.)<br/>
We will use Mural for most of the Sprint collaboration. Some of the things we will do in Mural:
- * Create artifacts like affinity diagrams from participants' input
- * Use post-its to comment on each other's points and to add notes
- * Vote on ideas and solutions
+ * Create artifacts like affinity diagrams from participants' input.
+ * Use post-its to comment on each other's points and to add notes.
+ * Vote on ideas and solutions.
* Create the first draft of the prototype.
-The Mural link to the collaboration project will be provided in the issue before the start of the Design Sprint.
+ * **The Mural link can be found here:** (Mural Link)
* **Video and/or screen recording tool** (Loom, Quicktime, Zoom or another tool you are using).<br/>
-As part of the pre-Sprint homework, you will be asked to record a short Lightning Walkthrough video. You can use any tool you feel comfortable with as long as it can capture your screen, mouse pointer and your audio.
+As part of the pre-Sprint homework, you will be asked to record a short Lightning Walkthrough video (don't worry, this will be explained in detail during the Sprint :smile:). You can use any tool you feel comfortable with as long as it can capture your screen, mouse pointer, and audio.
-* **A4/Letter sized paper (preferably white blank), Sharpies/Pens** (please don't use a pencil because it doesn't create enough contrast for photos).<br/>
-Day 2 of the sprint involves some (async) ideation via sketching so you will need a writing utensil (Sharpies are preferred because they force you to draw at a lower fidelity because the small details aren't necessary at this point) and some paper. This is the most fun part of the Sprint where you get into a design thinking mindset and can appeal to your creative self. Don't worry, it's not about artistry, it's about ideas and collaboration.
+* **A4/Letter-sized paper (preferably white, blank), Sharpies/Pens** (Please don't use a pencil because it doesn't create enough contrast for photos).<br/>
+Day 2 of the Sprint involves some (async) ideation via sketching so you will need a writing utensil (Sharpies are preferred because they force you to draw at a lower fidelity because the small details aren't necessary at this point) and some paper. This is the most fun part of the Sprint where you get into a design thinking mindset and can appeal to your creative self. Don't worry, it's not about artistry, it's about ideas and collaboration. It'll be fun!
* **Camera (phone or other) or scanner**<br/>
You will need to upload sketches as images for the facilitator to prepare the material before the next sync meeting. You can take a photo with your phone or use a scanner if available.
* **Post-it notes (Optional)**<br/>
-If you enjoy taking notes using post-it notes make sure you have available some of them as well. The upside is that they will make you feel more like you are in a workshop and will help the ideas flow (I find that typing is distracting while ideating). The downside is that you will have to digitalise the ones you want to share with the team in Mural.
+If you enjoy taking notes using Post-it notes make sure you have some of them as well. The upside is that they will make you feel more like you are in a workshop and will help the ideas flow (I find that typing is distracting while ideating). The downside is that you will have to digitize the ones you want to share with the team in Mural.
## Artefacts & Pre-Read Material
-<!-- If there is material that will be useful for the participants to read before the Design Sprint add here. -->
+<!-- If there is material that will be useful for the participants to read before the Design Sprint add here, such as the sprint slide deck or design sprint material -->
### Handbook pages
<!-- Add a link to the category vision from the handbook -->
### Competitor resources
-<!-- Add any solutions by competitors that are relevant to the Design Sprint topic and could be used as source of inspiration. -->
-
+<!-- Add any solutions by competitors that are relevant to the Design Sprint topic and could be used as a source of inspiration. -->
### Articles on Design Sprints
* [The Design Sprint](https://www.gv.com/sprint/)
* [The Ultimate Guide To Remote Design Sprints](https://drive.google.com/file/d/16bwrAqHVf8qxovd87Q7LdzqwAgy7a6Rx/view?usp=sharing)
-## Asyncronus tasks
-
-### Design Sprint preparation
-
-<!-- Replace the roles with GitLab handles to assign to specific participants -->
-
-- [ ] Finalise participant list - `decider` and `facilitator`
-- [ ] Create [participation form](https://docs.google.com/forms/d/e/1FAIpQLSc0_BNltvRW8yXXaJd8sIKzgDmrSGqILMfkoCJrAj6sFcsMcg/viewform?usp=sf_link) and send to participants (**deadline**: [date]) - `facilitator`
-- [ ] Create a dedicated Slack channel and add participants - `facilitator`
-- [ ] Promote this issue to an epic - `facilitator`
-- [ ] Create issues under the epic for the pre-workshop tasks: Expert interviews ([example](https://gitlab.com/groups/gitlab-org/configure/-/epics/3#note_332412524)), Lightning walkthroughs and How might we.. notetaking assignment ([example](https://gitlab.com/gitlab-org/configure/general/-/issues/52)), Voting How might we... notes assignment ([example](https://gitlab.com/gitlab-org/configure/general/-/issues/54)) - `facilitator`
-- [ ] Create sync meetings in calendar and invite all participants (**deadline**: [date]) - `decider` or `facilitator`
-- [ ] Block 2 hours for Sprint activities in calendar for the Sprint week - `all participants`
-- [ ] Prepare material and tools (eg. presentation templates, Google folders, Instructions videos etc) and Mural board from the [Mural template ](https://app.mural.co/invitation/mural/gitlab2474/1586990879319?sender=jmandell0210&key=03c25e92-9a43-4a3d-8907-6f0c3b094ab8) - facilitator
-- [ ] Finalize Agenda - `facilitator`
-- [ ] Run a test with material and tools - `facilitator`
-- [ ] Start user recruiting for prototype user testing (EOD 1) - `facilitator` or `decider`
-
-### Pre-Sprint activities (Homework exercises)
-
-Each exercise should be explained and documented in a separate issue. You can use the example issues above as templates.
-
-- [ ] Fill form and submit (**deadline**: [date]) - `all participants except the facilitator`
-- [ ] Expert interview analysis - `facilitator`
-- [ ] Lightning walkthrough videos (**deadline**: [date]) - `all participants except the facilitator`
-- [ ] How might we... notetaking assignment (**deadline**: [date]) - `all participants except the facilitator`
-- [ ] Voting How might we... notes assignment (**deadline**: [date]) - `all participants except the facilitator`
-- [ ] Add all required material to the Mural board (**deadline**: [date]) - `facilitator`
-
-### During Sprint activities
-
-- [ ] Organise user testing sessions - `facilitator` or `decider`
-- [ ] Create the Prototype to be tested and task list (End of Day 3) - `Product designer` or `Front end developer`
-- [ ] Run user testing sessions - `facilitator` or `decider`
-
-### Post-Sprint activities
-
-- [ ] Create a feedback issue for the Design Sprint - `facilitator` or `decider`
-- [ ] Analyse user testing results - `facilitator` or `decider`
-- [ ] Create report and share with the Design Sprint participants and wider team - `facilitator` or `decider`
-
## Personas
Deciding which persona we are focusing on will be part of the Day 1 discussions in the workshop. The personas we are going to consider are:
-<!-- Choose which personas could be target users so that you choose from this list during the Sprint. Personas are described at https://about.gitlab.com/handbook/product/personas/
+<!-- Choose which personas could be target users, and choose from this list during the Sprint. Personas are described at https://about.gitlab.com/handbook/product/personas/
* [Parker (Product Manager)](https://about.gitlab.com/handbook/product/personas/#parker-product-manager)
* [Delaney (Development Team Lead)](https://about.gitlab.com/handbook/product/personas/#delaney-development-team-lead)
@@ -161,44 +117,162 @@ Deciding which persona we are focusing on will be part of the Day 1 discussions
-->
-## Agenda
-### Day 1
-
- | Activity | Duration | Tool | Description |
-|---|---|---|---|
-| Warm-up exercise | 5 mins | Mural | Write 1 post-it answering the questions: <br/>"My name is…"<br/>"My role is…"<br/>“Something about myself you may not know is…”<br/>"My wish for this workshop is…" |
-| Summarise the async activities & complete Map | 20 mins | Mural | The Map is intended to show the focus of the Sprint and doesn't need to be complete or detailed. Steps:<br/> Go through the Map and the top voted How might we’s tree as a warm-up/reminder. <br/> • Make appropriate adjustments and additions to the map based on the reviews from the team. <br/> • Add the most voted HMWs to the most relevant area on the Map. If a HMW can go to more than one place, add it to the most left area. |
-| Long term goals/Deciding the Sprint goal | 15 mins | Mural | • Long term goal: Everyone spends 5 minutes in silence and writes one (max 2) long term goal post-it note for the Sprint. (5 mins ) <br/> • One by one everyone will read their goal aloud to the team. (5 mins) <br/> Everyone besides the decider will vote on the goal of the Sprint (1 dot). (4 minutes) <br/> The decider then makes the final decision on the long term goal of the Sprint. (1 min) |
- | Sprint questions | 20 mins | Mural | • Referencing the Long Term goal, everyone will write 2-3 post-it note Sprint questions for the biggest challenges they think might stop us from achieving our long term goal (what might hold us back or hinder us from achieving this goal). The questions should start with “Can we...” (similarly to the HMW). (7 mins) <br/> • One by one everyone will read their Sprint questions aloud to the team. (5 mins) <br/> • Everyone (including the decider) votes on the top 3 questions they think we should focus on as Sprint challenges (3 dots). (5 mins) <br/> • Separate the 3 most voted questions and keep them on the side. (1 min) <br/> • Finally, the decider chooses one Sprint question that will be the question we will focus on more during the Sprint by placing a green smiley sticker on it. (1 min) <br/> • Move the long term goal and the Sprint questions to the dedicated Mural space, highlighting the ultimate Sprint question that the decider chose. (1 min) |
-| Recap day. <br/> Short intro to next day and share the video with the next day exercise instructions. | 5 mins | Mural, Zoom | Summarise activities of the day and decisions. Brief walkthrough of the next day's activities and wrap up the day. |
-
-### Day 2
- | Activity | Duration | Tool | Description |
-|---|---|---|---|
-| Summary of Day 1 outcomes | 5 mins | Mural | Go through the previous day's activities, the Long term goal and the top voted Sprint questions, highlighting the ultimate Sprint question, and summarise the concept solution sketching homework exercise. |
-| Concept gallery review | 20 mins | Mural | • Everyone takes some time to read through and look at every aspect of each of the sketches in the Concept Gallery. The concept sketches are anonymous to avoid bias (15 mins). <br/> • The team will then vote on their favorite concepts and/or components of a concept via the red dots. When they see something that interests them and they think it will help solve the long term goal/challenges they can add one or more dots. They can use as many red dots as they want. Be frivolous when adding dots but if you really like or think something is important, add more to draw attention to it (5 mins).<br/> Note: If anyone has any questions about a concept sketch create a red sticky and write that question down placing it under the concept sketches. |
-| Speed critique | 5 mins | Mural | • The facilitator walks through each of the concepts, briefly summarizing each concept (to the best of their ability) with a focus on the areas that have been dotted. <br/> • During this time the facilitator will also try to answer any of the red post-it questions. <br/> • When the facilitator believes they’ve reached the end of their summary for that concept, ask the team if there was a concept that was voted on but not discussed or if the point of the red dot vote was missed in the discussion.
-| Straw Poll | ~15 mins | Mural | • All the participants, besides the Decider, vote using the larger green dot by adding their initials to it and placing it on the concept sketch they believe is the best one that will best fulfill the long term goal and challenges of this sprint and is worthy of being prototyped (2 mins) <br/> • Each participant will create a post-it note that explains their reasoning for choosing the concept. (5 mins) <br/> • Each participant will then get 1 minute to read through their post-it and attempt to sell their preferred concept to the Decider and the other participants. (5-10 mins) |
-| Super Vote (The Decider) | 10 mins | Mural | • The Decider makes their final decision of which of the concepts is the one to move forward with. <br/> • The decider can discuss their thought process and any questions with the rest of the participants. <br/> • They will get 2 green smiley stickers to vote with. Placing one on the concept they want to move forward with and the second, optional smiley, can be used to mark any other area of any other concept they think should also be incorporated into the prototype. |
-
-
-### Day 3
- | Activity | Duration | Tool | Description |
-|---|---|---|---|
-| User test flow | 25 mins | Mural | • Each participant writes (on separate post-its) 6 steps/actions that represent a step of a flow (you can think of a high-level prototype flow) from start to finish. Place them in the appropriate location on the User Test section. (10 mins) <br/> • Each participant takes 1 minute to walk the team through their flows one-by-one (5-10 mins total). Note: It's best to have the Decider go last. <br/> • Voting: All the Sprint participants get one red dot (the Decider gets 2) to vote on the flow row they think is the best foundation for the prototype. <br/> • After everyone has voted the Decider will vote on the row they think is best with one dot using the second dot to, optionally, vote on an element of another flow they think should be incorporated into the prototype. (5 mins) <br/> • If the second dot is used copy the specific sticky into the main flow voted by the Decider. |
-| Storyboard | 45 mins | Mural | • Copy the winning flow from the User Test Flow exercise to the Storyboard/Prototype section placing each individual post-it note into its own container. <br/> • Look at the sketch concepts and move over any relevant screens that fulfill the needs of the sticky note in that container. You can move the entire concept or screen capture cut/paste parts of concepts. Note: Don’t add any unnecessary details or ideas that aren’t needed for the end result prototype <br/> • Fill in the details that are required for each step described in the sticky. |
-| Recap day | 5 mins | Mural, Zoom | Summarise activities of the day and decisions. Brief walkthrough of the next day's activities and wrap up the day. |
-
-### Day 4
- | Activity | Duration | Tool | Description |
-|---|---|---|---|
-| Validate Prototype | 30 mins | Mural | • Go through the Prototype created by the Product designer or Front end developer and discuss any inaccuracies or missing content. |
-| Wrap up the Sprint | 15 mins | Zoom, GitLab | • Recap the Sprint and discuss next steps. Create user testing issues. |
-
-### Day 5
- | Activity | Duration | Tool | Description |
-|---|---|---|---|
-| Prototype testing with 5 users | ~45 mins | Figma or code/Zoom | • Test the prototype with users. |
+## Pre-Sprint Preparation | Due Date: `Add Date`
+
+- [ ] Promote this issue to an epic.
+- [ ] Finalize objective and outputs.
+- [ ] Finalize participant list.
+- [ ] Create a dedicated Slack channel and add participants.
+- [ ] Create issues for each day of the sprint.
+- [ ] Prepare activity slide deck.
+- [ ] Prepare Mural Board and ensure participant access.
+- [ ] Prepare instructional videos for activities (Lightning Talks, HMWs, Crazy 8s etc).
+- [ ] Organize and inform team members who will be providing lightning talk recordings.
+- [ ] Open a recruitment issue for user testing.
+- [ ] Create sync meetings in calendars for all participants.
+- [ ] Record Kickoff Video.
+- [ ] Do a Sprint test run.
+
+## Sprint Kickoff | `Add Date`
+
+#### Pre-Day Facilitator Checklist:
+- [ ] Ensure [Kickoff Issue](Add Link) is complete.
+- [ ] Assign participants to the [Kickoff Issue](Add Link).
+- [ ] Ensure warm-up exercise Mural is set up.
+- [ ] Prep Q&A issue thread.
+- [ ] Send a `Welcome to the Sprint` slack message.
+
+ | Activity | Type of Activity | Duration | Tool | Description |
+|---|---|---|---|---|
+| Introduction to Design Sprints | Async | 20 Minutes | Video Recording | • The facilitator gives an overview of the Sprint, including details about what they are, why they are used, and why they can help solve the problem for this Sprint. <br/> • The facilitator gives participants an overview of what they need to complete a Design Sprint such as a run down of the rules, and supples, as well as an agenda and basic expectations. <br/> • The facilitator gives participants an overview of the problem we will be solving and allows for an async Q&A period. |
+| Icebreaker | Async | 5 Minutes | Mural | • All Sprint participants give an async introduction in Mural. |
+| Q&A Period | Async | 5 Minutes | GitLab and Slack | • Participants can ask questions about the Sprint. (Optional) |
+| Record Lightning Talk | Async | 10 Minutes | Zoom | • Participants record their Lightning Talk. |
+
+#### Post-Day Facilitator Checklist:
+- [ ] Answer any questions that come up during the Q&A period.
+- [ ] Send a homework reminder in the Slack channel.
+
+## Day 1 - Sprint Homework | `Add Date`
+
+#### Pre-Day Facilitator Checklist:
+- [ ] Ensure [Day 1 Issue](Add Link) is complete.
+- [ ] Ensure Lightning Talk recordings are added to the [Day 1 Issue](Add Link).
+- [ ] Ensure participants have access to the Note Taking Activity Walkthrough Video.
+- [ ] Ensure HMW section on Mural is organized.
+- [ ] Ensure participants have access to the HMW Activity Walkthrough video.
+
+ | Activity | Type of Activity | Duration | Tool | Description |
+|---|---|---|---|---|
+| Watch Lightning Talks | Async | 15 Minutes Per Talk | Video Recording and Note Taking Tool | • Participants will watch lightning talks and complete the note-taking activity async. <br/> • The Facilitator will provide an overview video of what is expected during the note taking portion of this activity. |
+| How Might We’s (HMWs) | Async | 20 Minutes | Mural | • Participants will take their notes from the Lightning Talk activity and craft some HMWs around the opportunities uncovered. <br/> • The facilitator will provide participants a video to describe the HMW activity and review what makes a good HMW statement. <br/> • The facilitator will also be available to sync on Slack for any questions around the activity. |
+
+#### Post-Day Facilitator Checklist:
+- [ ] Answer any questions that come up during the async HMW activity.
+- [ ] Send a sync session reminder in the Slack channel.
+
+## Day 1 - Sync Session | `Add Date`
+
+#### Pre-Day Facilitator Checklist:
+- [ ] Ensure all sync Mural boards are organized (Affinity Mapping, Goals, Hurdles, Squiggle Birds).
+- [ ] Ensure sync session is recorded.
+
+ | Activity | Type of Activity | Duration | Tool | Description |
+|---|---|---|---|---|
+| Affinity Mapping | Sync | 30 Minutes | Zoom and Mural | • Participants take turns reading aloud their HMWs. (5 minutes). <br/> • Once the review has completed, the facilitator will choose an Affinity Mapper who will be in charge of categorizing the HMWs with the help of the entire group (10 Minutes). <br/> • Once the HMWs are organized into categories, the group will do a round of dot voting on the categories we want to focus on for the Sprint. Each participant will get three votes, and vote individually on the category they feel is most important to work on (10 Minutes). <br/> • The facilitator will allow time for discussion around the HMW groups chosen, and see if there needs to be adjustments (5 Minutes). |
+| Sprint Goal | Sync | 20 Minutes | Zoom and Mural | • Based on the outcomes of the HMW activity, participants start by asking themselves the following question: “If everything worked out perfectly, what would that look like for this project?” <br/> • Each participant writes one Sprint goal on a Mural sticky starting with “By the end of the Sprint…” (10 Minutes) <br/> • Everyone on the team should share their Sprint Goal with the larger group and post them on the Mural (5 Minutes). <br/> • Everyone is given one dot to vote on what they think the Sprint Goal should be. (5 Minutes) |
+| Sprint Hurdles | Sync | 20 Minutes | Zoom and Mural | • Participants will list out possible critical hurdles, in the form of a question, that may stop you from reaching your goal: What could stop us or heavily impact us from reaching our goals? <br/> • Each question must start with ‘Can we...’ <br/> • Each person can only write 2 questions (10 Minutes) <br/> • Everyone on the team should share their Hurdles back to the larger group and post them up on the Mural (5 Minutes). <br/> • Everyone is given three to vote on what they think are the most important to focus on as challenges toward the Goal. (5 Minutes) |
+| Recap of Day | Sync | 5 Minutes | Zoom and Mural | • Facilitator gives and overview of what has been completed so far during Day 1 of the Sprint and expectations for Day 2 Homework |
+| Squiggle Birds | Sync | 5 Minutes | Zoom, Paper and Pen/Sharpie | • Sync warm-up drawing activity to prep for next async tasks. <br/> • The facilitator will draw a squiggle on Mural and encourage all participants to also make a squiggle. <br/> • Everyone will then turn the squiggle into a bird. <br/> • The facilitator will share that "Our minds are great at recognizing patterns." Sketches are only used to convey an idea, so they don't need to be super detailed or accurate. |
+
+#### Post-Day Facilitator Checklist:
+- [ ] Send a Day 1 overview and homework reminder in the Slack channel.
+- [ ] Post-sync session recording in the Slack channel.
+- [ ] Ensure [Day 2 Issue](Add Link) is complete.
+- [ ] Ensure participants have access to the Crazy 8s Walkthrough Video.
+- [ ] Ensure the Crazy 8s section on Mural is organized.
+
+## Day 2 - Sprint Homework | (Add Date)
+
+#### Pre-Day Facilitator Checklist:
+- [ ] Ensure Ideation recordings are added to the [Day 2 Issue](Add Link).
+- [ ] Ensure participants have access to the Ideation Walkthrough Videos.
+- [ ] Ensure the Ideation section on Mural is organized.
+
+| Activity | Type of Activity | Duration | Tool | Description |
+|---|---|---|---|---|
+| Note Recap | Async | Google Docs | 5 Minutes | • Review all the notes that were taken during Day 1. <br/> • Review all the HMW stickies to jog your memory. |
+| Ideas List | Async | Google Docs | 5 Minutes | • On a piece of paper jot down any solutions that come to mind around solving HMW statements. |
+| Crazy 8s | Async | Paper and Marker/Pen | 8 Minutes | • Grab your printer paper and create an 8-panel page by folding your paper in half 3 times. <br/> • Draw a sketch for an idea in each rectangle. <br/> • Start at the top of your HMW statements/ideas list from the previous activity.|
+| Share-out Prep | Async | Mural | 10 Minutes | • In preparation for the share-out, take a photo of your Crazy 8 paper and add it to the Mural. |
+
+#### Post-Day Facilitator Checklist:
+- [ ] Send a sync session reminder in the Slack channel.
+
+## Day 2 - Sync Session | `Add Date`
+
+#### Pre-Day Facilitator Checklist:
+
+- [ ] Ensure sync Murals are organized (Crazy 8s, Storyboarding).
+- [ ] Ensure sync session is recorded.
+
+| Activity | Type of Activity | Duration | Tool | Description |
+|---|---|---|---|---|
+| Crazy 8's Share out and Voting | Sync | 15 Minutes | Zoom and Mural | • Each person will have two minutes to present their ideas to the team. <br/> • After each team member has gotten the chance to present their ideas, we will do a round of dot voting. <br/> • Vote on the ideas you think will be best to solve our Sprint problem. <br/> • Each team member will get 3 votes. |
+| Storyboarding Key Moments | Sync - 2 Groups | 30 Minutes | Zoom, Paper and Sharpie/Pen | • Take the first 10 minutes to write down the 8-10 key moments individually. <br/> • As a team decide on the ideal storyboard together. <br/> • Try and bring out the task that needs to be done and the emotion that you would like the user to experience. <br/> • No drawing yet! |
+| Storyboard Details | Sync | 20 Minutes | Zoom, Paper and Pen/Sharpie | • Take your draft storyboard and give it more detail. <br/> • DRAW BIG! Sketch 1 key moment per 8.5x11 page. <br/> • Each person on the team should sketch at least 1 key moment. <br/> • Focus on actions and emotions. |
+| Put Together the Story | Sync | 10 Minutes | Zoom and Mural | • Each person will take a photo of their storyboard and add it to the Mural. |
+| Storyboard Share Out and Voting | Sync | 15 Minutes | Zoom and Mural | • Each group takes turns presenting their story to the Sprint team. <br/> • Once both teams have had the chance to present their storyboard, we’ll hold a round of voting. <br/> • Each person will be given 3 votes to vote for any part of each experience they like. <br/> • The Decider gets 6 votes for this activity, 3 mega-likes and 3 dislikes. |
+| All in One or Rumble | Sync | 5 Minutes | Zoom | • Decide as a group if you want to incorporate the best parts of both storyboards into one or if you want to test both storyboards against each other during the testing phase. |
+
+#### Post-Day Facilitator Checklist:
+- [ ] Send a Day 2 overview and homework reminder in the Slack channel.
+- [ ] Post-sync session recording in the Slack channel.
+- [ ] Ensure [Day 3 Issue](Add Link) is complete.
+
+## Day 3 - Group Sprint Homework | (Add Date)
+
+#### Post-Day Facilitator Checklist:
+- [ ] Set up a Figma File for the prototype and ensure it's shared with the group.
+
+| Activity | Type of Activity | Duration | Tool | Description |
+|---|---|---|---|---|
+| Prototyping | Async | 1 Hour | Figma | • The Designer will craft out a prototype based on the storyboard for the team to review. |
+| Prototyping Feedback | Async | 10 Minutes | Figma | • Once the Designer has completed the prototype, the team will review it and leave feedback. <br/> • There will only be a single round of feedback before hallway testing (where we will receive additional feedback from users). |
+| Review Hallway Testing Slides | Async | 10 Minutes | Slides | • Review the details on what Hallway Testing is to prepare for the next sync session. |
+
+#### Post-Day Facilitator Checklist:
+- [ ] Send a sync session reminder in the Slack channel.
+
+## Day 3 - Sync Session | (Add Date)
+
+#### Pre-Day Facilitator Checklist:
+
+- [ ] Ensure sync Murals are organized (Hallway Testing Prep).
+- [ ] Ensure sync session is recorded.
+- [ ] Ensure the prototype is done and has been adjusted from the feedback.
+- [ ] Gathered a few potential Hallway Test Volunteers.
+
+| Activity | Type of Activity | Duration | Tool | Description |
+|---|---|---|---|---|
+| Define a Research Objective | Sync | 10 Minutes | Zoom and Mural | • As a group, write a research objective for your hallway test. Make sure to capture: What you want to learn, Who you want to talk to, What you plan to do with what you learn. |
+| Write a Test Scenario | Sync | 10 Minutes | Zoom and Mural | • As a group, write a test scenario for your hallway test. <br/> • Write down a list of key tasks that you’d like the participant to complete while using the prototype. |
+| Write Test Questions | Sync | 10 Minutes | Zoom and Mural | • As a group, write a test scenario for your hallway test. <br/> • Craft a list of questions and / or tasks that you’ll ask participants to answer or complete in order to answer your research objectives. |
+| Hallway Testing | Sync | 50 Minutes | Zoom | • Working in groups of 2-3, review the discussion guide and prototype. <br/> • Determine who will ask questions and who will take notes. Remember to switch roles after each test! <br/> • It’s up to you where you take notes. <br/> • Join the Zoom meeting with your assigned person & test your prototype. <br/> • Have the team document what they hear and be prepared to share back with the larger sprint team. |
+| Hallway Testing Share-out | Sync | 20 Minutes | Zoom | • Each group will have roughly 5 minutes to present their findings from Hallway Testing. Each team should present: <br/> • What were the common themes? <br/> • What did you hear that was surprising? <br/> • Any red flags? <br/> • Thank participants for their time. |
+
+#### Post-Day Facilitator Checklist:
+- [ ] Summarize themes of Hallway Testing and share with the Slack channel and the [Day 3 Issue](Add Link).
+- [ ] Optionally, share with UX, Product, and other relevant Slack channels.
+- [ ] Share with team next steps related to Day 4 and Usertesting.
+
+## Day 4 - Usertesting | (Add Date)
+
+<!-- Day 4 can happen at a later date when it makes the most sense. Use the problem validation issue template for Day 4. -->
+
+Day 4 consists of a round of user testing which typically happens a week or more after the first three days complete.
+
+* **Day Four Issue:** (Day 4 Issue Link)
## Ground Rules
* Honor the Facilitator's directions. They're the guide for the entire process.
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index 7d6bcfbe200..43b33acc045 100644
--- a/app/assets/javascripts/boards/components/board_app.vue
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -6,8 +6,8 @@ import BoardContent from '~/boards/components/board_content.vue';
import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
import eventHub from '~/boards/eventhub';
-import { listsQuery } from 'ee_else_ce/boards/constants';
-import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
+import { listsQuery, FilterFields } from 'ee_else_ce/boards/constants';
+import { formatBoardLists, filterVariables, FiltersInfo } from 'ee_else_ce/boards/boards_util';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import errorQuery from '../graphql/client/error.query.graphql';
import { setError } from '../graphql/cache_updates';
@@ -137,7 +137,12 @@ export default {
setFilters(filters) {
const filterParams = { ...filters };
if (filterParams.groupBy) delete filterParams.groupBy;
- this.filterParams = filterParams;
+ this.filterParams = filterVariables({
+ filters: filterParams,
+ issuableType: this.issuableType,
+ filterInfo: FiltersInfo,
+ filterFields: FilterFields,
+ });
},
},
};
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 18495f285da..b514330fcdd 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -113,8 +113,8 @@ export default {
this.$apollo.mutate({
mutation: setActiveBoardItemMutation,
variables: {
- boardItem: this.item,
- isIssue: this.isIssueBoard,
+ boardItem: this.isActive ? null : this.item,
+ isIssue: this.isActive ? undefined : this.isIssueBoard,
},
});
},
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index f5e1e51cd0c..682d380cc20 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -8,6 +8,7 @@ import { defaultSortableOptions } from '~/sortable/constants';
import { sortableStart, sortableEnd } from '~/sortable/utils';
import Tracking from '~/tracking';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
+import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
@@ -49,6 +50,7 @@ export default {
mixins: [Tracking.mixin(), glFeatureFlagMixin()],
inject: [
'isEpicBoard',
+ 'isIssueBoard',
'isGroupBoard',
'disabled',
'fullPath',
@@ -578,6 +580,7 @@ export default {
async addListItem(input) {
this.toggleForm();
this.addItemToListInProgress = true;
+ let issuable;
try {
await this.$apollo.mutate({
mutation: listIssuablesQueries[this.issuableType].createMutation,
@@ -586,7 +589,7 @@ export default {
withColor: this.isEpicBoard && this.glFeatures.epicColorHighlight,
},
update: (cache, { data: { createIssuable } }) => {
- const { issuable } = createIssuable;
+ issuable = createIssuable.issuable;
addItemToList({
query: listIssuablesQueries[this.issuableType].query,
variables: { ...this.listQueryVariables, id: this.currentList.id },
@@ -626,6 +629,13 @@ export default {
});
} finally {
this.addItemToListInProgress = false;
+ this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: {
+ boardItem: issuable,
+ isIssue: this.isIssueBoard,
+ },
+ });
}
},
},
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 2f87e3fd44f..ae12ca5feed 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -94,9 +94,12 @@ export default {
parentType() {
return this.boardType;
},
- boardQuery() {
+ issueBoardsQuery() {
return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery;
},
+ boardsQuery() {
+ return this.issueBoardsQuery;
+ },
loading() {
return this.loadingRecentBoards || this.loadingBoards;
},
@@ -161,7 +164,7 @@ export default {
if (!data?.[this.parentType]) {
return [];
}
- return data[this.parentType][boardType].edges.map(({ node }) => ({
+ return data[this.parentType][boardType].nodes.map((node) => ({
id: getIdFromGraphQLId(node.id),
name: node.name,
}));
@@ -178,7 +181,7 @@ export default {
variables() {
return { fullPath: this.fullPath };
},
- query: this.boardQuery,
+ query: this.boardsQuery,
update: (data) => this.boardUpdate(data, 'boards'),
watchLoading: (isLoading) => {
this.loadingBoards = isLoading;
@@ -217,19 +220,19 @@ export default {
const { defaultClient: store } = this.$apollo.provider.clients;
const sourceData = store.readQuery({
- query: this.boardQuery,
+ query: this.boardsQuery,
variables: { fullPath: this.fullPath },
});
const newData = produce(sourceData, (draftState) => {
- draftState[this.parentType].boards.edges = [
- ...draftState[this.parentType].boards.edges,
- { node: board },
+ draftState[this.parentType].boards.nodes = [
+ ...draftState[this.parentType].boards.nodes,
+ { ...board },
];
});
store.writeQuery({
- query: this.boardQuery,
+ query: this.boardsQuery,
variables: { fullPath: this.fullPath },
data: newData,
});
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index dd3b9472879..b6ee3871b4f 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -29,7 +29,7 @@ export default {
return this.canAdminList ? s__('Boards|Edit board') : s__('Boards|View scope');
},
tooltipTitle() {
- return this.hasScope ? __("This board's scope is reduced") : '';
+ return this.hasScope || this.boardHasScope ? __("This board's scope is reduced") : '';
},
},
methods: {
diff --git a/app/assets/javascripts/boards/graphql/group_boards.query.graphql b/app/assets/javascripts/boards/graphql/group_boards.query.graphql
index ce9f7bbfd2a..0fb5748abfc 100644
--- a/app/assets/javascripts/boards/graphql/group_boards.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_boards.query.graphql
@@ -2,11 +2,9 @@ query group_boards($fullPath: ID!) {
group(fullPath: $fullPath) {
id
boards {
- edges {
- node {
- id
- name
- }
+ nodes {
+ id
+ name
}
}
}
diff --git a/app/assets/javascripts/boards/graphql/group_recent_boards.query.graphql b/app/assets/javascripts/boards/graphql/group_recent_boards.query.graphql
index b9fe778d4d4..9dbf4528cec 100644
--- a/app/assets/javascripts/boards/graphql/group_recent_boards.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_recent_boards.query.graphql
@@ -2,11 +2,9 @@ query group_recent_boards($fullPath: ID!) {
group(fullPath: $fullPath) {
id
recentIssueBoards {
- edges {
- node {
- id
- name
- }
+ nodes {
+ id
+ name
}
}
}
diff --git a/app/assets/javascripts/boards/graphql/project_boards.query.graphql b/app/assets/javascripts/boards/graphql/project_boards.query.graphql
index 770c246a95b..97a298db246 100644
--- a/app/assets/javascripts/boards/graphql/project_boards.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_boards.query.graphql
@@ -2,11 +2,9 @@ query project_boards($fullPath: ID!) {
project(fullPath: $fullPath) {
id
boards {
- edges {
- node {
- id
- name
- }
+ nodes {
+ id
+ name
}
}
}
diff --git a/app/assets/javascripts/boards/graphql/project_recent_boards.query.graphql b/app/assets/javascripts/boards/graphql/project_recent_boards.query.graphql
index c633107a409..0d3a8616603 100644
--- a/app/assets/javascripts/boards/graphql/project_recent_boards.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_recent_boards.query.graphql
@@ -2,11 +2,9 @@ query project_recent_boards($fullPath: ID!) {
project(fullPath: $fullPath) {
id
recentIssueBoards {
- edges {
- node {
- id
- name
- }
+ nodes {
+ id
+ name
}
}
}
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index a1efeaac359..8c30520ebec 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -24,6 +24,10 @@ export default {
required: true,
type: Object,
},
+ environmentName: {
+ required: true,
+ type: String,
+ },
namespace: {
required: false,
type: String,
@@ -96,7 +100,12 @@ export default {
</p>
<gl-collapse :visible="isVisible" class="gl-md-pl-7 gl-md-pr-5 gl-mt-4">
<template v-if="isVisible">
- <kubernetes-status-bar :cluster-health-status="clusterHealthStatus" class="gl-mb-4" />
+ <kubernetes-status-bar
+ :cluster-health-status="clusterHealthStatus"
+ :configuration="k8sAccessConfiguration"
+ :namespace="namespace"
+ :environment-name="environmentName"
+ class="gl-mb-3" />
<kubernetes-agent-info :cluster-agent="clusterAgent" class="gl-mb-5" />
<gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-5">
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index 94cd7438e46..b8859e65e38 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -1,7 +1,9 @@
<script>
import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
import { s__ } from '~/locale';
-import { HEALTH_BADGES } from '../constants';
+import { HEALTH_BADGES, SYNC_STATUS_BADGES, STATUS_TRUE, STATUS_FALSE } from '../constants';
+import fluxKustomizationStatusQuery from '../graphql/queries/flux_kustomization_status.query.graphql';
+import fluxHelmReleaseStatusQuery from '../graphql/queries/flux_helm_release_status.query.graphql';
export default {
components: {
@@ -17,23 +19,137 @@ export default {
return ['error', 'success', ''].includes(val);
},
},
+ configuration: {
+ required: true,
+ type: Object,
+ },
+ environmentName: {
+ required: true,
+ type: String,
+ },
+ namespace: {
+ required: false,
+ type: String,
+ default: '',
+ },
+ },
+ apollo: {
+ fluxKustomizationStatus: {
+ query: fluxKustomizationStatusQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ namespace: this.namespace,
+ environmentName: this.environmentName.toLowerCase(),
+ };
+ },
+ skip() {
+ return !this.namespace;
+ },
+ },
+ fluxHelmReleaseStatus: {
+ query: fluxHelmReleaseStatusQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ namespace: this.namespace,
+ environmentName: this.environmentName.toLowerCase(),
+ };
+ },
+ skip() {
+ return Boolean(
+ !this.namespace ||
+ this.$apollo.queries.fluxKustomizationStatus.loading ||
+ this.hasKustomizations,
+ );
+ },
+ },
},
computed: {
healthBadge() {
return HEALTH_BADGES[this.clusterHealthStatus];
},
+ hasKustomizations() {
+ return this.fluxKustomizationStatus?.length;
+ },
+ hasHelmReleases() {
+ return this.fluxHelmReleaseStatus?.length;
+ },
+ isLoading() {
+ return (
+ this.$apollo.queries.fluxKustomizationStatus.loading ||
+ this.$apollo.queries.fluxHelmReleaseStatus.loading
+ );
+ },
+ fluxCRD() {
+ if (!this.hasKustomizations && !this.hasHelmReleases) {
+ return [];
+ }
+
+ return this.hasKustomizations ? this.fluxKustomizationStatus : this.fluxHelmReleaseStatus;
+ },
+ fluxAnyStalled() {
+ return this.fluxCRD.find((condition) => {
+ return condition.status === STATUS_TRUE && condition.type === 'Stalled';
+ });
+ },
+ fluxAnyReconciling() {
+ return this.fluxCRD.find((condition) => {
+ return condition.status === STATUS_TRUE && condition.type === 'Reconciling';
+ });
+ },
+ fluxAnyReconciled() {
+ return this.fluxCRD.find((condition) => {
+ return condition.status === STATUS_TRUE && condition.type === 'Ready';
+ });
+ },
+ fluxAnyFailed() {
+ return this.fluxCRD.find((condition) => {
+ return condition.status === STATUS_FALSE && condition.type === 'Ready';
+ });
+ },
+ syncStatusBadge() {
+ if (!this.fluxCRD.length) {
+ return SYNC_STATUS_BADGES.unavailable;
+ } else if (this.fluxAnyFailed) {
+ return SYNC_STATUS_BADGES.failed;
+ } else if (this.fluxAnyStalled) {
+ return SYNC_STATUS_BADGES.stalled;
+ } else if (this.fluxAnyReconciling) {
+ return SYNC_STATUS_BADGES.reconciling;
+ } else if (this.fluxAnyReconciled) {
+ return SYNC_STATUS_BADGES.reconciled;
+ }
+ return SYNC_STATUS_BADGES.unknown;
+ },
},
i18n: {
healthLabel: s__('Environment|Environment health'),
+ syncStatusLabel: s__('Environment|Sync status'),
},
+ badgeContainerClasses: 'gl-display-flex gl-align-items-center gl-flex-shrink-0 gl-mr-3 gl-mb-2',
};
</script>
<template>
- <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-mb-2">
- <span class="gl-font-sm gl-font-monospace gl-mr-3">{{ $options.i18n.healthLabel }}</span>
- <gl-loading-icon v-if="!clusterHealthStatus" size="sm" inline />
- <gl-badge v-else-if="healthBadge" :variant="healthBadge.variant">
- {{ healthBadge.text }}
- </gl-badge>
+ <div class="gl-display-flex gl-flex-wrap">
+ <div :class="$options.badgeContainerClasses">
+ <span class="gl-mr-3">{{ $options.i18n.healthLabel }}</span>
+ <gl-loading-icon v-if="!clusterHealthStatus" size="sm" inline />
+ <gl-badge v-else-if="healthBadge" :variant="healthBadge.variant" data-testid="health-badge">
+ {{ healthBadge.text }}
+ </gl-badge>
+ </div>
+
+ <div :class="$options.badgeContainerClasses">
+ <span class="gl-mr-3">{{ $options.i18n.syncStatusLabel }}</span>
+ <gl-loading-icon v-if="isLoading" size="sm" inline />
+ <gl-badge
+ v-else-if="syncStatusBadge"
+ :icon="syncStatusBadge.icon"
+ :variant="syncStatusBadge.variant"
+ data-testid="sync-badge"
+ >{{ syncStatusBadge.text }}</gl-badge
+ >
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index fda1c85f739..555d525c3b6 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -372,7 +372,11 @@ export default {
</gl-sprintf>
</div>
<div v-if="clusterAgent" :class="$options.kubernetesOverviewClasses">
- <kubernetes-overview :cluster-agent="clusterAgent" :namespace="kubernetesNamespace" />
+ <kubernetes-overview
+ :cluster-agent="clusterAgent"
+ :namespace="kubernetesNamespace"
+ :environment-name="environment.name"
+ />
</div>
<div v-if="rolloutStatus" :class="$options.deployBoardClasses">
<deploy-board-wrapper
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index dc9481a5429..921be902bdc 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -104,6 +104,42 @@ export const HEALTH_BADGES = {
},
};
+export const SYNC_STATUS_BADGES = {
+ reconciled: {
+ variant: 'success',
+ icon: 'status_success',
+ text: s__('Environment|Reconciled'),
+ },
+ reconciling: {
+ variant: 'info',
+ icon: 'status_running',
+ text: s__('Environment|Reconciling'),
+ },
+ stalled: {
+ variant: 'warning',
+ icon: 'status_pending',
+ text: s__('Environment|Stalled'),
+ },
+ failed: {
+ variant: 'danger',
+ icon: 'status_failed',
+ text: s__('Deployment|Failed'),
+ },
+ unknown: {
+ variant: 'neutral',
+ icon: 'status_notfound',
+ text: s__('Deployment|Unknown'),
+ },
+ unavailable: {
+ variant: 'muted',
+ icon: 'status_notfound',
+ text: s__('Deployment|Unavailable'),
+ },
+};
+
+export const STATUS_TRUE = 'True';
+export const STATUS_FALSE = 'False';
+
export const PHASE_RUNNING = 'Running';
export const PHASE_PENDING = 'Pending';
export const PHASE_SUCCEEDED = 'Succeeded';
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index 553b06e632f..8faed710402 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -9,6 +9,8 @@ import k8sPodsQuery from './queries/k8s_pods.query.graphql';
import k8sServicesQuery from './queries/k8s_services.query.graphql';
import k8sWorkloadsQuery from './queries/k8s_workloads.query.graphql';
import k8sNamespacesQuery from './queries/k8s_namespaces.query.graphql';
+import fluxKustomizationStatusQuery from './queries/flux_kustomization_status.query.graphql';
+import fluxHelmReleaseStatusQuery from './queries/flux_helm_release_status.query.graphql';
import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql';
@@ -170,6 +172,21 @@ export const apolloProvider = (endpoint) => {
},
},
});
+ cache.writeQuery({
+ query: fluxKustomizationStatusQuery,
+ data: {
+ status: '',
+ type: '',
+ },
+ });
+ cache.writeQuery({
+ query: fluxHelmReleaseStatusQuery,
+ data: {
+ status: '',
+ type: '',
+ },
+ });
+
return new VueApollo({
defaultClient,
});
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
new file mode 100644
index 00000000000..0857f6f57df
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
@@ -0,0 +1,14 @@
+query getFluxHelmReleaseStatusQuery(
+ $configuration: LocalConfiguration
+ $namespace: String
+ $environmentName: String
+) {
+ fluxHelmReleaseStatus(
+ configuration: $configuration
+ namespace: $namespace
+ environmentName: $environmentName
+ ) @client {
+ status
+ type
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
new file mode 100644
index 00000000000..4eaea014718
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
@@ -0,0 +1,14 @@
+query getFluxHelmKustomizationStatusQuery(
+ $configuration: LocalConfiguration
+ $namespace: String
+ $environmentName: String
+) {
+ fluxKustomizationStatus(
+ configuration: $configuration
+ namespace: $namespace
+ environmentName: $environmentName
+ ) @client {
+ status
+ type
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index 8cfe44c5a05..fcc17cea4e1 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -16,6 +16,11 @@ import environmentToChangeCanaryQuery from './queries/environment_to_change_cana
import isEnvironmentStoppingQuery from './queries/is_environment_stopping.query.graphql';
import pageInfoQuery from './queries/page_info.query.graphql';
+const helmReleasesResourceType = 'helmreleases';
+const kustomizationsResourceType = 'kustomizations';
+const helmReleasesApiVersion = 'helm.toolkit.fluxcd.io/v2beta1';
+const kustomizationsApiVersion = 'kustomize.toolkit.fluxcd.io/v1beta1';
+
const buildErrors = (errors = []) => ({
errors,
__typename: 'LocalEnvironmentErrors',
@@ -78,6 +83,30 @@ const handleClusterError = (err) => {
throw error;
};
+const buildFluxResourceUrl = ({
+ basePath,
+ namespace,
+ apiVersion,
+ resourceType,
+ environmentName,
+}) => {
+ return `${basePath}/apis/${apiVersion}/namespaces/${namespace}/${resourceType}/${environmentName}`;
+};
+
+const getFluxResourceStatus = (configuration, url) => {
+ const { headers } = configuration.baseOptions;
+ const withCredentials = true;
+
+ return axios
+ .get(url, { withCredentials, headers })
+ .then((res) => {
+ return res?.data?.status?.conditions || [];
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
export const resolvers = (endpoint) => ({
Query: {
environmentApp(_context, { page, scope, search }, { cache }) {
@@ -223,6 +252,26 @@ export const resolvers = (endpoint) => ({
throw new Error(humanizeClusterErrors(error));
});
},
+ fluxKustomizationStatus(_, { configuration, namespace, environmentName }) {
+ const url = buildFluxResourceUrl({
+ basePath: configuration.basePath,
+ resourceType: kustomizationsResourceType,
+ apiVersion: kustomizationsApiVersion,
+ namespace,
+ environmentName,
+ });
+ return getFluxResourceStatus(configuration, url);
+ },
+ fluxHelmReleaseStatus(_, { configuration, namespace, environmentName }) {
+ const url = buildFluxResourceUrl({
+ basePath: configuration.basePath,
+ resourceType: helmReleasesResourceType,
+ apiVersion: helmReleasesApiVersion,
+ namespace,
+ environmentName,
+ });
+ return getFluxResourceStatus(configuration, url);
+ },
},
Mutation: {
stopEnvironmentREST(_, { environment }, { client }) {
diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql
index e2c22dda554..41f165ad1da 100644
--- a/app/assets/javascripts/environments/graphql/typedefs.graphql
+++ b/app/assets/javascripts/environments/graphql/typedefs.graphql
@@ -167,6 +167,11 @@ type LocalK8sNamespaces {
metadata: k8sNamespaceMetadata
}
+type LocalFluxResourceStatus {
+ status: String
+ type: String
+}
+
extend type Query {
environmentApp(page: Int, scope: String): LocalEnvironmentApp
folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder
@@ -179,6 +184,16 @@ extend type Query {
k8sPods(configuration: LocalConfiguration, namespace: String): [LocalK8sPods]
k8sServices(configuration: LocalConfiguration): [LocalK8sServices]
k8sWorkloads(configuration: LocalConfiguration, namespace: String): LocalK8sWorkloads
+ fluxKustomizationStatus(
+ configuration: LocalConfiguration
+ namespace: String
+ environmentName: String
+ ): LocalFluxResourceStatus
+ fluxHelmReleaseStatus(
+ configuration: LocalConfiguration
+ namespace: String
+ environmentName: String
+ ): LocalFluxResourceStatus
}
extend type Mutation {
diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
index a4ec48ffd2f..d396169c0a3 100644
--- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue
+++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
@@ -1,5 +1,5 @@
<script>
-import { GlToggle, GlAlert } from '@gitlab/ui';
+import { GlAlert, GlLink, GlSprintf, GlToggle } from '@gitlab/ui';
import { sprintf } from '~/locale';
import { updateGroup } from '~/api/groups_api';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@@ -14,19 +14,29 @@ import {
export default {
components: {
- GlToggle,
GlAlert,
+ GlLink,
+ GlSprintf,
+ GlToggle,
+ },
+ inject: {
+ groupId: {},
+ groupName: {},
+ groupIsEmpty: {},
+ sharedRunnersSetting: {},
+
+ runnerEnabledValue: {},
+ runnerDisabledValue: {},
+ runnerAllowOverrideValue: {},
+
+ // Parent group, only present in sub-groups
+
+ parentSharedRunnersSetting: { default: null },
+
+ // Available when user can admin parent
+ parentName: { default: null },
+ parentSettingsPath: { default: null },
},
- inject: [
- 'groupId',
- 'groupName',
- 'groupIsEmpty',
- 'sharedRunnersSetting',
- 'parentSharedRunnersSetting',
- 'runnerEnabledValue',
- 'runnerDisabledValue',
- 'runnerAllowOverrideValue',
- ],
data() {
return {
isLoading: false,
@@ -48,6 +58,9 @@ export default {
overrideToggleValue() {
return this.value === this.runnerAllowOverrideValue;
},
+ isParentAvailable() {
+ return this.parentSettingsPath && this.parentName;
+ },
},
methods: {
async onSharedRunnersToggle(enabled) {
@@ -109,26 +122,28 @@ export default {
<gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-5">
{{ error }}
</gl-alert>
-
- <gl-alert
- v-if="isSharedRunnersToggleDisabled"
- variant="warning"
- :dismissible="false"
- class="gl-mb-5"
- >
- {{ __('Shared runners are disabled for the parent group') }}
- </gl-alert>
-
<section class="gl-mb-5">
<gl-toggle
:value="sharedRunnersToggleValue"
:is-loading="isLoading"
:disabled="isSharedRunnersToggleDisabled"
:label="__('Enable shared runners for this group')"
- :help="__('Enable shared runners for all projects and subgroups in this group.')"
+ :description="__('Enable shared runners for all projects and subgroups in this group.')"
data-testid="shared-runners-toggle"
@change="onSharedRunnersToggle"
- />
+ >
+ <template v-if="isSharedRunnersToggleDisabled" #help>
+ {{ s__('Runners|Shared runners are disabled.') }}
+ <gl-sprintf
+ v-if="isParentAvailable"
+ :message="s__('Runners|Go to %{groupLink} to enable them.')"
+ >
+ <template #groupLink>
+ <gl-link :href="parentSettingsPath">{{ parentName }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-toggle>
</section>
<section class="gl-mb-5">
@@ -137,10 +152,24 @@ export default {
:is-loading="isLoading"
:disabled="isOverrideToggleDisabled"
:label="__('Allow projects and subgroups to override the group setting')"
- :help="__('Allows projects or subgroups in this group to override the global setting.')"
+ :description="
+ __('Allows projects or subgroups in this group to override the global setting.')
+ "
data-testid="override-runners-toggle"
@change="onOverrideToggle"
- />
+ >
+ <template v-if="isSharedRunnersToggleDisabled" #help>
+ {{ s__('Runners|Shared runners are disabled.') }}
+ <gl-sprintf
+ v-if="isParentAvailable"
+ :message="s__('Runners|Go to %{groupLink} to enable them.')"
+ >
+ <template #groupLink>
+ <gl-link :href="parentSettingsPath">{{ parentName }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-toggle>
</section>
</div>
</template>
diff --git a/app/assets/javascripts/group_settings/mount_shared_runners.js b/app/assets/javascripts/group_settings/mount_shared_runners.js
index 0767330cd54..334192a6f87 100644
--- a/app/assets/javascripts/group_settings/mount_shared_runners.js
+++ b/app/assets/javascripts/group_settings/mount_shared_runners.js
@@ -10,6 +10,8 @@ export default (containerId = 'update-shared-runners-form') => {
groupName,
groupIsEmpty,
sharedRunnersSetting,
+ parentName,
+ parentSettingsPath,
parentSharedRunnersSetting,
runnerEnabledValue,
runnerDisabledValue,
@@ -23,10 +25,14 @@ export default (containerId = 'update-shared-runners-form') => {
groupName,
groupIsEmpty: parseBoolean(groupIsEmpty),
sharedRunnersSetting,
- parentSharedRunnersSetting,
+
runnerEnabledValue,
runnerDisabledValue,
runnerAllowOverrideValue,
+
+ parentName,
+ parentSettingsPath,
+ parentSharedRunnersSetting,
},
render(createElement) {
return createElement(UpdateSharedRunnersForm);
diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue
index ac359b4f901..234ac0505b2 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -5,6 +5,7 @@ import {
GlCard,
GlFormInput,
GlLink,
+ GlIcon,
GlLoadingIcon,
GlSprintf,
GlToggle,
@@ -64,6 +65,7 @@ export default {
GlCard,
GlFormInput,
GlLink,
+ GlIcon,
GlLoadingIcon,
GlSprintf,
GlToggle,
@@ -109,6 +111,7 @@ export default {
inboundJobTokenScopeEnabled: null,
targetProjectPath: '',
projects: [],
+ isAddFormVisible: false,
};
},
computed: {
@@ -193,10 +196,14 @@ export default {
},
clearTargetProjectPath() {
this.targetProjectPath = '';
+ this.isAddFormVisible = false;
},
getProjects() {
this.$apollo.queries.projects.refetch();
},
+ showAddForm() {
+ this.isAddFormVisible = true;
+ },
},
};
</script>
@@ -228,22 +235,55 @@ export default {
</gl-toggle>
<div>
- <gl-card class="gl-mt-5 gl-mb-3">
+ <gl-card
+ class="gl-new-card"
+ header-class="gl-new-card-header"
+ body-class="gl-new-card-body gl-px-0"
+ >
<template #header>
- <h5 class="gl-my-0">{{ $options.i18n.cardHeaderTitle }}</h5>
+ <div class="gl-new-card-title-wrapper">
+ <h5 class="gl-new-card-title">{{ $options.i18n.cardHeaderTitle }}</h5>
+ <span class="gl-new-card-count">
+ <gl-icon name="project" class="gl-mr-2" />
+ {{ projects.length }}
+ </span>
+ </div>
+ <div class="gl-new-card-actions">
+ <gl-button
+ v-if="!isAddFormVisible"
+ size="small"
+ data-testid="toggle-form-btn"
+ @click="showAddForm"
+ >{{ $options.i18n.addProject }}</gl-button
+ >
+ </div>
</template>
- <template #default>
+
+ <div v-if="isAddFormVisible" class="gl-new-card-add-form gl-m-3">
+ <h4 class="gl-mt-0">{{ $options.i18n.addProject }}</h4>
<gl-form-input
v-model="targetProjectPath"
:placeholder="$options.i18n.addProjectPlaceholder"
/>
- </template>
- <template #footer>
- <gl-button variant="confirm" :disabled="isProjectPathEmpty" @click="addProject">
- {{ $options.i18n.addProject }}
- </gl-button>
- <gl-button @click="clearTargetProjectPath">{{ $options.i18n.cancel }}</gl-button>
- </template>
+ <div class="gl-display-flex gl-mt-5">
+ <gl-button
+ variant="confirm"
+ :disabled="isProjectPathEmpty"
+ class="gl-mr-3"
+ data-testid="add-project-btn"
+ @click="addProject"
+ >
+ {{ $options.i18n.addProject }}
+ </gl-button>
+ <gl-button @click="clearTargetProjectPath">{{ $options.i18n.cancel }}</gl-button>
+ </div>
+ </div>
+
+ <token-projects-table
+ :projects="projects"
+ :table-fields="$options.fields"
+ @removeProject="removeProject"
+ />
</gl-card>
<gl-alert
v-if="!inboundJobTokenScopeEnabled"
@@ -254,11 +294,6 @@ export default {
>
{{ $options.i18n.settingDisabledMessage }}
</gl-alert>
- <token-projects-table
- :projects="projects"
- :table-fields="$options.fields"
- @removeProject="removeProject"
- />
</div>
</template>
</div>
diff --git a/app/assets/javascripts/token_access/components/outbound_token_access.vue b/app/assets/javascripts/token_access/components/outbound_token_access.vue
index cad1fff062a..7e1e6cc445c 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -3,8 +3,8 @@ import {
GlAlert,
GlButton,
GlCard,
- GlFormInput,
GlLink,
+ GlIcon,
GlLoadingIcon,
GlSprintf,
GlToggle,
@@ -71,8 +71,8 @@ export default {
GlAlert,
GlButton,
GlCard,
- GlFormInput,
GlLink,
+ GlIcon,
GlLoadingIcon,
GlSprintf,
GlToggle,
@@ -221,7 +221,7 @@ export default {
<gl-loading-icon v-if="$apollo.loading" size="lg" class="gl-mt-5" />
<template v-else>
<gl-alert
- class="gl-mb-3"
+ class="gl-mt-5 gl-mb-3"
variant="warning"
:dismissible="false"
:show-icon="false"
@@ -268,30 +268,29 @@ export default {
</gl-toggle>
<div>
- <gl-card class="gl-mt-5 gl-mb-3">
+ <gl-card
+ class="gl-new-card"
+ header-class="gl-new-card-header"
+ body-class="gl-new-card-body gl-px-0"
+ >
<template #header>
- <h5 class="gl-my-0">{{ $options.i18n.cardHeaderTitle }}</h5>
- </template>
- <template #default>
- <gl-form-input
- v-model="targetProjectPath"
- :disabled="true"
- :placeholder="$options.i18n.addProjectPlaceholder"
- data-testid="project-path-input"
- />
- </template>
- <template #footer>
- <gl-button variant="confirm" :disabled="isProjectPathEmpty" @click="addProject">
- {{ $options.i18n.addProject }}
- </gl-button>
- <gl-button @click="clearTargetProjectPath">{{ $options.i18n.cancel }}</gl-button>
+ <div class="gl-new-card-title-wrapper">
+ <h5 class="gl-new-card-title">{{ $options.i18n.cardHeaderTitle }}</h5>
+ <span class="gl-new-card-count">
+ <gl-icon name="project" class="gl-mr-2" />
+ {{ projects.length }}
+ </span>
+ </div>
+ <div class="gl-new-card-actions">
+ <gl-button size="small" disabled>{{ $options.i18n.addProject }}</gl-button>
+ </div>
</template>
+ <token-projects-table
+ :projects="projects"
+ :table-fields="$options.fields"
+ @removeProject="removeProject"
+ />
</gl-card>
- <token-projects-table
- :projects="projects"
- :table-fields="$options.fields"
- @removeProject="removeProject"
- />
</div>
</template>
</div>
diff --git a/app/assets/javascripts/token_access/components/token_access_app.vue b/app/assets/javascripts/token_access/components/token_access_app.vue
index 167eebc8d9b..d2d5e6b2a5a 100644
--- a/app/assets/javascripts/token_access/components/token_access_app.vue
+++ b/app/assets/javascripts/token_access/components/token_access_app.vue
@@ -12,7 +12,7 @@ export default {
</script>
<template>
<div>
- <inbound-token-access class="gl-pb-5" />
- <outbound-token-access class="gl-py-5" />
+ <inbound-token-access />
+ <outbound-token-access />
</div>
</template>
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index b1481f668bb..9085ff5f45a 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -76,16 +76,29 @@ module Ci
end
def group_shared_runners_settings_data(group)
- {
+ data = {
group_id: group.id,
group_name: group.name,
group_is_empty: (group.projects.empty? && group.children.empty?).to_s,
shared_runners_setting: group.shared_runners_setting,
- parent_shared_runners_setting: group.parent&.shared_runners_setting,
+
runner_enabled_value: Namespace::SR_ENABLED,
runner_disabled_value: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
- runner_allow_override_value: Namespace::SR_DISABLED_AND_OVERRIDABLE
+ runner_allow_override_value: Namespace::SR_DISABLED_AND_OVERRIDABLE,
+
+ parent_shared_runners_setting: group.parent&.shared_runners_setting,
+ parent_name: nil,
+ parent_settings_path: nil
}
+
+ if group.parent && can?(current_user, :admin_group, group.parent)
+ data.merge!({
+ parent_name: group.parent.name,
+ parent_settings_path: group_settings_ci_cd_path(group.parent, anchor: 'js-runner-settings')
+ })
+ end
+
+ data
end
def group_runners_data_attributes(group)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index a0e5eefafa6..371b34a8749 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -412,6 +412,7 @@ module Ci
joins(:pipeline_metadata).where(name_column.eq(name))
end
+ scope :for_status, -> (status) { where(status: status) }
scope :created_after, -> (time) { where(arel_table[:created_at].gt(time)) }
scope :created_before_id, -> (id) { where(arel_table[:id].lt(id)) }
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
diff --git a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
index e197821a0c0..0c035ef3ff0 100644
--- a/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
+++ b/app/services/ci/pipeline_creation/cancel_redundant_pipelines_service.rb
@@ -67,6 +67,7 @@ module Ci
.for_ref(pipeline.ref)
.where_not_sha(project.commit(pipeline.ref).try(:id))
.where("created_at < ?", pipeline.created_at)
+ .for_status(CommitStatus::AVAILABLE_STATUSES)
.ci_sources
scope = scope.id_in(ids) if ids.present?
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 007169809c9..d0ce6a2c846 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -106,7 +106,7 @@
= _("Token Access")
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
- %p
+ %p.gl-text-secondary
= _("Control how the CI_JOB_TOKEN CI/CD variable is used for API access between projects.")
.settings-content
= render 'ci/token_access/index'
diff --git a/doc/api/pipeline_triggers.md b/doc/api/pipeline_triggers.md
index c62e622e31e..6f1cdbc440d 100644
--- a/doc/api/pipeline_triggers.md
+++ b/doc/api/pipeline_triggers.md
@@ -43,7 +43,7 @@ user. Trigger tokens created by other users are shortened to four characters.
## Get trigger token details
-Get details of a project's pipeline trigger.
+Get details of a project's pipeline trigger token.
```plaintext
GET /projects/:id/triggers/:trigger_id
@@ -72,7 +72,7 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
## Create a trigger token
-Create a pipeline trigger for a project.
+Create a pipeline trigger token for a project.
```plaintext
POST /projects/:id/triggers
@@ -100,9 +100,9 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
}
```
-## Update a project trigger token
+## Update a pipeline trigger token
-Update a pipeline trigger token for a project.
+Update a project's pipeline trigger token.
```plaintext
PUT /projects/:id/triggers/:trigger_id
@@ -131,7 +131,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
}
```
-## Remove a project trigger token
+## Remove a pipeline trigger token
Remove a project's pipeline trigger token.
@@ -150,7 +150,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://git
## Trigger a pipeline with a token
-Trigger a pipeline by using a pipeline [trigger token](../ci/triggers/index.md#create-a-trigger-token)
+Trigger a pipeline by using a [pipeline trigger token](../ci/triggers/index.md#create-a-pipeline-trigger-token)
or a [CI/CD job token](../ci/jobs/ci_job_token.md) for authentication.
With a CI/CD job token, the [triggered pipeline is a multi-project pipeline](../ci/pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api).
diff --git a/doc/ci/triggers/index.md b/doc/ci/triggers/index.md
index 3970db8fe53..c955097f6f5 100644
--- a/doc/ci/triggers/index.md
+++ b/doc/ci/triggers/index.md
@@ -12,12 +12,12 @@ to the [pipeline triggers API endpoint](../../api/pipeline_triggers.md).
When authenticating with the API, you can use:
-- A [trigger token](#create-a-trigger-token) to trigger a branch or tag pipeline.
+- A [pipeline trigger token](#create-a-pipeline-trigger-token) to trigger a branch or tag pipeline.
- A [CI/CD job token](../jobs/ci_job_token.md) to [trigger a multi-project pipeline](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api).
-## Create a trigger token
+## Create a pipeline trigger token
-You can trigger a pipeline for a branch or tag by generating a trigger token and using it
+You can trigger a pipeline for a branch or tag by generating a pipeline trigger token and using it
to authenticate an API call. The token impersonates a user's project access and permissions.
Prerequisite:
@@ -41,12 +41,12 @@ to improve the security of trigger tokens.
## Trigger a pipeline
-After you [create a trigger token](#create-a-trigger-token), you can use it to trigger
+After you [create a pipeline trigger token](#create-a-pipeline-trigger-token), you can use it to trigger
pipelines with a tool that can access the API, or a webhook.
### Use cURL
-You can use cURL to trigger pipelines with the [pipeline triggers API endpoint](../../api/pipeline_triggers.md).
+You can use cURL to trigger pipelines with the [pipeline trigger token API endpoint](../../api/pipeline_triggers.md).
For example:
- Use a multiline cURL command:
@@ -62,7 +62,7 @@ For example:
```shell
curl --request POST \
- "https://gitlab.example.com/api/v4/projects/<project_id>/trigger/pipeline?token=<token>&ref=<ref_name>"
+ "https://gitlab.example.com/api/v4/projects/<project_id>/trigger/pipeline?token=<token>&ref=<ref_name>"
```
In each example, replace:
@@ -75,7 +75,7 @@ In each example, replace:
### Use a CI/CD job
-You can use a CI/CD job with a triggers token to trigger pipelines when another pipeline
+You can use a CI/CD job with a pipeline triggers token to trigger pipelines when another pipeline
runs.
For example, to trigger a pipeline on the `main` branch of `project-B` when a tag
@@ -116,9 +116,9 @@ Replace:
- `<ref_name>` with a branch or tag name, like `main`. This value takes precedence over the `ref_name` in the webhook payload.
The payload's `ref` is the branch that fired the trigger in the source repository.
You must URL-encode the `ref_name` if it contains slashes.
-- `<token>` with your trigger token.
+- `<token>` with your pipeline trigger token.
-#### Use a webhook payload
+#### Access webhook payload
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31197) in GitLab 13.9.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/321027) in GitLab 13.11.
@@ -138,10 +138,10 @@ The parameter is of the form `variables[key]=value`, for example:
```shell
curl --request POST \
- --form token=TOKEN \
- --form ref=main \
- --form variables[UPLOAD_TO_S3]="true" \
- "https://gitlab.example.com/api/v4/projects/123456/trigger/pipeline"
+ --form token=TOKEN \
+ --form ref=main \
+ --form variables[UPLOAD_TO_S3]="true" \
+ "https://gitlab.example.com/api/v4/projects/123456/trigger/pipeline"
```
CI/CD variables in triggered pipelines display on each job's page, but only
@@ -149,9 +149,9 @@ users with the Owner and Maintainer role can view the values.
![Job variables in UI](img/trigger_variables.png)
-## Revoke a trigger token
+## Revoke a pipeline trigger token
-To revoke a trigger token:
+To revoke a pipeline trigger token:
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Settings > CI/CD**.
@@ -162,23 +162,24 @@ A revoked trigger token cannot be added back.
## Configure CI/CD jobs to run in triggered pipelines
-To [configure when to run jobs](../jobs/job_control.md) in triggered pipelines:
+To [configure when to run jobs](../jobs/job_control.md) in triggered pipelines, you can:
- Use [`rules`](../yaml/index.md#rules) with the `$CI_PIPELINE_SOURCE` [predefined CI/CD variable](../variables/predefined_variables.md).
-- Use [`only`/`except`](../yaml/index.md#onlyrefs--exceptrefs) keywords.
+- Use [`only`/`except`](../yaml/index.md#onlyrefs--exceptrefs) keywords, though `rules`
+ is the preferred keyword.
| `$CI_PIPELINE_SOURCE` value | `only`/`except` keywords | Trigger method |
|-----------------------------|--------------------------|---------------------|
-| `trigger` | `triggers` | In pipelines triggered with the [pipeline triggers API](../../api/pipeline_triggers.md) by using a [trigger token](#create-a-trigger-token). |
+| `trigger` | `triggers` | In pipelines triggered with the [pipeline triggers API](../../api/pipeline_triggers.md) by using a [trigger token](#create-a-pipeline-trigger-token). |
| `pipeline` | `pipelines` | In [multi-project pipelines](../pipelines/downstream_pipelines.md#trigger-a-multi-project-pipeline-by-using-the-api) triggered with the [pipeline triggers API](../../api/pipeline_triggers.md) by using the [`$CI_JOB_TOKEN`](../jobs/ci_job_token.md), or by using the [`trigger`](../yaml/index.md#trigger) keyword in the CI/CD configuration file. |
Additionally, the `$CI_PIPELINE_TRIGGERED` predefined CI/CD variable is set to `true`
-in pipelines triggered with a trigger token.
+in pipelines triggered with a pipeline trigger token.
-## See which trigger token was used
+## See which pipeline trigger token was used
-You can see which trigger caused a job to run by visiting the single job page.
-A part of the trigger's token displays on the right of the page, under the job details:
+You can see which pipeline trigger token caused a job to run by visiting the single job page.
+A part of the trigger token displays on the right of the page, under the job details:
![Marked as triggered on a single job page](img/trigger_single_job.png)
@@ -196,7 +197,7 @@ To avoid trigger loops, do not use [pipeline events](../../user/project/integrat
A response of `{"message":"404 Not Found"}` when triggering a pipeline might be caused
by using a [personal access token](../../user/profile/personal_access_tokens.md)
-instead of a trigger token. [Create a new trigger token](#create-a-trigger-token)
+instead of a pipeline trigger token. [Create a new trigger token](#create-a-pipeline-trigger-token)
and use it instead of the personal access token.
### `The requested URL returned error: 400` when triggering a pipeline
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index 538e756b962..2cf6fe485ae 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -144,7 +144,7 @@ as it can cause the pipeline to behave unexpectedly.
| `GITLAB_USER_LOGIN` | 10.0 | all | The username of the user who started the pipeline, unless the job is a manual job. In manual jobs, the value is the username of the user who started the job. |
| `GITLAB_USER_NAME` | 10.0 | all | The name of the user who started the pipeline, unless the job is a manual job. In manual jobs, the value is the name of the user who started the job. |
| `KUBECONFIG` | 14.2 | all | The path to the `kubeconfig` file with contexts for every shared agent connection. Only available when a [GitLab agent is authorized to access the project](../../user/clusters/agent/ci_cd_workflow.md#authorize-the-agent). |
-| `TRIGGER_PAYLOAD` | 13.9 | all | The webhook payload. Only available when a pipeline is [triggered with a webhook](../triggers/index.md#use-a-webhook-payload). |
+| `TRIGGER_PAYLOAD` | 13.9 | all | The webhook payload. Only available when a pipeline is [triggered with a webhook](../triggers/index.md#access-webhook-payload). |
## Predefined variables for merge request pipelines
diff --git a/doc/development/pipelines/internals.md b/doc/development/pipelines/internals.md
index 904ac11511d..82e95392989 100644
--- a/doc/development/pipelines/internals.md
+++ b/doc/development/pipelines/internals.md
@@ -305,63 +305,79 @@ You can skip a job `git clone`/`git fetch` by adding the following pattern to a
#### Scenario 1: no `before_script` is defined in the job
+This applies to the parent sections the job extends from as well.
+
You can just extend the `.fast-no-clone-job`:
+**Before:**
+
```yaml
- extends:
- - .fast-no-clone-job
- variables:
- FILES_TO_DOWNLOAD: >
- scripts/rspec_helpers.sh
- scripts/slack
+ # Note: No `extends:` is present in the job
+ a-job:
+ script:
+ - source scripts/rspec_helpers.sh scripts/slack
+ - echo "No need for a git clone!"
```
-#### Scenario 2: a `before_script` block is already defined in the job
-
-You have to include the `.fast-no-clone-job` via a `!reference` as well:
+**After:**
```yaml
- extends:
- - .fast-no-clone-job
- variables:
- FILES_TO_DOWNLOAD: >
- scripts/rspec_helpers.sh
- scripts/slack
- before_script:
- - !reference [".fast-no-clone-job", before_script]
- - # [...]
+ # Note: No `extends:` is present in the job
+ a-job:
+ extends:
+ - .fast-no-clone-job
+ variables:
+ FILES_TO_DOWNLOAD: >
+ scripts/rspec_helpers.sh
+ scripts/slack
+ script:
+ - source scripts/rspec_helpers.sh scripts/slack
+ - echo "No need for a git clone!"
```
-- The job sets the `GIT_STRATEGY` to `none`.
-- The files are downloaded from current project, on the current `CI_COMMIT_SHA`
-- We use the `PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE` to fetch files from the repository (particularly important if we are in a private project)
+#### Scenario 2: a `before_script` block is already defined in the job (or in jobs it extends)
+
+For this scenario, you have to:
-Below is an example on how to convert a job using this pattern:
+1. Extend the `.fast-no-clone-job` as in the first scenario (this will merge the `FILES_TO_DOWNLOAD` variable with the other variables)
+1. Make sure the `before_script` section from `.fast-no-clone-job` is referenced in the `before_script` we use for this job.
+
+**Before:**
```yaml
-# Before
-my-job:
- image: ruby
- stage: prepare
- script: # This job requires two files to function
- - source ./scripts/rspec_helpers.sh
- - source ./scripts/slack
- - echo "The files were successfully sourced!"
-
-# After
-my-job:
- extends:
- - .fast-no-clone-job
- image: ruby
- stage: prepare
- variables:
- FILES_TO_DOWNLOAD: >
- scripts/rspec_helpers.sh
- scripts/slack
- script: # This job requires two files to function
- - source ./scripts/rspec_helpers.sh
- - source ./scripts/slack
- - echo "The files were successfully sourced!"
+ .base-job:
+ before_script:
+ echo "Hello from .base-job"
+
+ a-job:
+ extends:
+ - .base-job
+ script:
+ - source scripts/rspec_helpers.sh scripts/slack
+ - echo "No need for a git clone!"
+```
+
+**After:**
+
+```yaml
+ .base-job:
+ before_script:
+ echo "Hello from .base-job"
+
+ a-job:
+ extends:
+ - .base-job
+ - .fast-no-clone-job
+ variables:
+ FILES_TO_DOWNLOAD: >
+ scripts/rspec_helpers.sh
+ scripts/slack
+ before_script:
+ - !reference [".fast-no-clone-job", before_script]
+ - !reference [".base-job", before_script]
+ script:
+ - source scripts/rspec_helpers.sh scripts/slack
+ - echo "No need for a git clone!"
```
#### Caveats
diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md
index f4652d592f0..8cbda3df0d2 100644
--- a/doc/user/project/settings/project_access_tokens.md
+++ b/doc/user/project/settings/project_access_tokens.md
@@ -79,7 +79,10 @@ To revoke a project access token:
## Scopes for a project access token
-The scope determines the actions you can perform when you authenticate with a project access token.
+The scope determines the actions you can perform when you authenticate with a project access token.
+
+NOTE:
+See the warning in [create a project access token](#create-a-project-access-token) regarding internal projects.
| Scope | Description |
|:-------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 21886a633bf..3e99ae3fdcc 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -16277,6 +16277,12 @@ msgstr ""
msgid "Deployment|Triggerer"
msgstr ""
+msgid "Deployment|Unavailable"
+msgstr ""
+
+msgid "Deployment|Unknown"
+msgstr ""
+
msgid "Deployment|Waiting"
msgstr ""
@@ -18273,6 +18279,12 @@ msgstr ""
msgid "Environment|Ports"
msgstr ""
+msgid "Environment|Reconciled"
+msgstr ""
+
+msgid "Environment|Reconciling"
+msgstr ""
+
msgid "Environment|ReplicaSets"
msgstr ""
@@ -18282,6 +18294,9 @@ msgstr ""
msgid "Environment|Services"
msgstr ""
+msgid "Environment|Stalled"
+msgstr ""
+
msgid "Environment|StatefulSets"
msgstr ""
@@ -18291,6 +18306,9 @@ msgstr ""
msgid "Environment|Summary"
msgstr ""
+msgid "Environment|Sync status"
+msgstr ""
+
msgid "Environment|There was an error connecting to the cluster agent."
msgstr ""
@@ -40428,6 +40446,9 @@ msgstr ""
msgid "Runners|GitLab Runner must be installed before you can register a runner. %{linkStart}How do I install GitLab Runner?%{linkEnd}"
msgstr ""
+msgid "Runners|Go to %{groupLink} to enable them."
+msgstr ""
+
msgid "Runners|Go to runners page"
msgstr ""
@@ -40795,6 +40816,9 @@ msgstr ""
msgid "Runners|Shared runners are disabled in the group settings"
msgstr ""
+msgid "Runners|Shared runners are disabled."
+msgstr ""
+
msgid "Runners|Shared runners will be disabled for all projects and subgroups in this group. If you proceed, you must manually re-enable shared runners in the settings of each project and subgroup."
msgstr ""
@@ -43596,9 +43620,6 @@ msgstr ""
msgid "Shared runners"
msgstr ""
-msgid "Shared runners are disabled for the parent group"
-msgstr ""
-
msgid "Shared runners details"
msgstr ""
diff --git a/qa/Dockerfile b/qa/Dockerfile
index 213ec3450cb..92866ec66cc 100644
--- a/qa/Dockerfile
+++ b/qa/Dockerfile
@@ -49,6 +49,8 @@ COPY ./config/initializers/0_inject_enterprise_edition_module.rb /home/gitlab/co
COPY ./config/feature_flags /home/gitlab/config/feature_flags
COPY ./config/bundler_setup.rb /home/gitlab/config/
COPY ./lib/gitlab_edition.rb /home/gitlab/lib/
+COPY ./spec/support/fast_quarantine.rb /home/gitlab/spec/support/
+COPY ./tooling/lib/tooling/fast_quarantine.rb /home/gitlab/tooling/lib/tooling/
COPY ./INSTALLATION_TYPE ./VERSION /home/gitlab/
COPY ./qa /home/gitlab/qa
diff --git a/qa/qa/specs/spec_helper.rb b/qa/qa/specs/spec_helper.rb
index 1601dd46c62..7768966b19c 100644
--- a/qa/qa/specs/spec_helper.rb
+++ b/qa/qa/specs/spec_helper.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
-require_relative '../../qa'
require 'active_support/testing/time_helpers'
+require_relative '../../qa'
+
+# Require shared test tooling from Rails test suite
+require_relative '../../../spec/support/fast_quarantine'
+
QA::Specs::QaDeprecationToolkitEnv.configure!
Knapsack::Adapters::RSpecAdapter.bind if QA::Runtime::Env.knapsack?
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index f92b10a19ff..cab1ac10f35 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -13,9 +13,9 @@ function retrieve_tests_metadata() {
echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
fi
- if [[ ! -f "${RSPEC_FAST_QUARANTINE_LOCAL_PATH}" ]]; then
- curl --location -o "${RSPEC_FAST_QUARANTINE_LOCAL_PATH}" "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/${RSPEC_FAST_QUARANTINE_LOCAL_PATH}" ||
- echo "" > "${RSPEC_FAST_QUARANTINE_LOCAL_PATH}"
+ if [[ ! -f "${RSPEC_FAST_QUARANTINE_PATH}" ]]; then
+ curl --location -o "${RSPEC_FAST_QUARANTINE_PATH}" "https://gitlab-org.gitlab.io/quality/engineering-productivity/fast-quarantine/${RSPEC_FAST_QUARANTINE_PATH}" ||
+ echo "" > "${RSPEC_FAST_QUARANTINE_PATH}"
fi
}
@@ -179,7 +179,7 @@ function debug_rspec_variables() {
echoinfo "FLAKY_RSPEC_SUITE_REPORT_PATH: ${FLAKY_RSPEC_SUITE_REPORT_PATH:-}"
echoinfo "FLAKY_RSPEC_REPORT_PATH: ${FLAKY_RSPEC_REPORT_PATH:-}"
echoinfo "NEW_FLAKY_RSPEC_REPORT_PATH: ${NEW_FLAKY_RSPEC_REPORT_PATH:-}"
- echoinfo "SKIPPED_TESTS_REPORT_PATH: ${SKIPPED_TESTS_REPORT_PATH:-}"
+ echoinfo "RSPEC_SKIPPED_TESTS_REPORT_PATH: ${RSPEC_SKIPPED_TESTS_REPORT_PATH:-}"
echoinfo "CRYSTALBALL: ${CRYSTALBALL:-}"
@@ -258,7 +258,6 @@ function rspec_paralellized_job() {
export KNAPSACK_TEST_FILE_PATTERN=$(ruby -r./tooling/quality/test_level.rb -e "puts Quality::TestLevel.new(${spec_folder_prefixes}).pattern(:${test_level})")
export FLAKY_RSPEC_REPORT_PATH="${rspec_flaky_folder_path}all_${report_name}_report.json"
export NEW_FLAKY_RSPEC_REPORT_PATH="${rspec_flaky_folder_path}new_${report_name}_report.json"
- export SKIPPED_TESTS_REPORT_PATH="rspec/skipped_tests_${report_name}.txt"
if [[ -d "ee/" ]]; then
export KNAPSACK_GENERATE_REPORT="true"
@@ -302,13 +301,10 @@ function retry_failed_rspec_examples() {
# Keep track of the tests that are retried, later consolidated in a single file by the `rspec:flaky-tests-report` job
local failed_examples=$(grep " failed" ${RSPEC_LAST_RUN_RESULTS_FILE})
local report_name=$(echo "${CI_JOB_NAME}" | sed -E 's|[/ ]|_|g') # e.g. 'rspec unit pg13 1/24' would become 'rspec_unit_pg13_1_24'
- local rspec_flaky_folder_path="$(dirname "${FLAKY_RSPEC_SUITE_REPORT_PATH}")/"
-
- export RETRIED_TESTS_REPORT_PATH="${rspec_flaky_folder_path}retried_tests_${report_name}_report.txt"
- echoinfo "RETRIED_TESTS_REPORT_PATH: ${RETRIED_TESTS_REPORT_PATH}"
+ echoinfo "RSPEC_RETRIED_TESTS_REPORT_PATH: ${RSPEC_RETRIED_TESTS_REPORT_PATH:-}"
- echo "${CI_JOB_URL}" > "${RETRIED_TESTS_REPORT_PATH}"
- echo $failed_examples >> "${RETRIED_TESTS_REPORT_PATH}"
+ echo "${CI_JOB_URL}" > "${RSPEC_RETRIED_TESTS_REPORT_PATH:-}"
+ echo $failed_examples >> "${RSPEC_RETRIED_TESTS_REPORT_PATH:-}"
echoinfo "Retrying the failing examples in a new RSpec process..."
@@ -357,7 +353,7 @@ function warn_on_successfully_retried_test {
# include the root path in the regexp to eliminate false positives
changed_file="^\./$changed_file"
- if grep -q "$changed_file" "$RETRIED_TESTS_REPORT_PATH"; then
+ if grep -q "${changed_file}" "${RSPEC_RETRIED_TESTS_REPORT_PATH}"; then
echoinfo "Flaky test '$changed_file' was found in the list of files changed by this MR."
echoinfo "Exiting with code $SUCCESSFULLY_RETRIED_TEST_EXIT_CODE."
exit $SUCCESSFULLY_RETRIED_TEST_EXIT_CODE
@@ -454,22 +450,20 @@ function cleanup_individual_job_reports() {
rm -rf ${knapsack_folder_path:-unknown_folder}rspec*.json \
${rspec_flaky_folder_path:-unknown_folder}all_*.json \
${rspec_flaky_folder_path:-unknown_folder}new_*.json \
- ${rspec_flaky_folder_path:-unknown_folder}skipped_flaky_tests_*_report.txt \
- ${rspec_flaky_folder_path:-unknown_folder}retried_tests_*_report.txt \
+ rspec/skipped_flaky_tests_*_report.txt \
+ rspec/retried_tests_*_report.txt \
${RSPEC_LAST_RUN_RESULTS_FILE:-unknown_folder} \
${RSPEC_PROFILING_FOLDER_PATH:-unknown_folder}/**/*
rmdir ${RSPEC_PROFILING_FOLDER_PAT:-unknown_folder} || true
}
function generate_flaky_tests_reports() {
- local rspec_flaky_folder_path="$(dirname "${FLAKY_RSPEC_SUITE_REPORT_PATH}")/"
-
debug_rspec_variables
- mkdir -p ${rspec_flaky_folder_path}
+ mkdir -p rspec/
- find ${rspec_flaky_folder_path} -type f -name 'skipped_tests_*.txt' -exec cat {} + >> "${SKIPPED_TESTS_REPORT_PATH}"
- find ${rspec_flaky_folder_path} -type f -name 'retried_tests_*_report.txt' -exec cat {} + >> "${RETRIED_TESTS_REPORT_PATH}"
+ find rspec/ -type f -name 'skipped_tests-*.txt' -exec cat {} + >> "rspec/skipped_tests_report.txt"
+ find rspec/ -type f -name 'retried_tests-*.txt' -exec cat {} + >> "rspec/retried_tests_report.txt"
cleanup_individual_job_reports
}
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 74d91eeaa26..50860c6e45b 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -180,8 +180,7 @@ describe('BoardsSelector', () => {
it('shows only matching boards when filtering', async () => {
const filterTerm = 'board1';
- const expectedCount = boards.filter((board) => board.node.name.includes(filterTerm))
- .length;
+ const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
fillSearchBox(filterTerm);
diff --git a/spec/frontend/boards/components/config_toggle_spec.js b/spec/frontend/boards/components/config_toggle_spec.js
index 5330721451e..9f006ebf01d 100644
--- a/spec/frontend/boards/components/config_toggle_spec.js
+++ b/spec/frontend/boards/components/config_toggle_spec.js
@@ -2,6 +2,7 @@ import Vuex from 'vuex';
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
import ConfigToggle from '~/boards/components/config_toggle.vue';
import eventHub from '~/boards/eventhub';
import store from '~/boards/stores';
@@ -12,13 +13,14 @@ describe('ConfigToggle', () => {
Vue.use(Vuex);
- const createComponent = (provide = {}) =>
+ const createComponent = (provide = {}, props = {}) =>
shallowMount(ConfigToggle, {
store,
provide: {
canAdminList: true,
...provide,
},
+ propsData: props,
});
const findButton = () => wrapper.findComponent(GlButton);
@@ -52,4 +54,20 @@ describe('ConfigToggle', () => {
label: 'edit_board',
});
});
+
+ it.each`
+ boardHasScope
+ ${true}
+ ${false}
+ `('renders dot highlight and tooltip depending on boardHasScope prop', ({ boardHasScope }) => {
+ wrapper = createComponent({}, { boardHasScope });
+
+ expect(findButton().classes('dot-highlight')).toBe(boardHasScope);
+
+ if (boardHasScope) {
+ expect(findButton().attributes('title')).toBe(__("This board's scope is reduced"));
+ } else {
+ expect(findButton().attributes('title')).toBe('');
+ }
+ });
});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 8235c3e4194..8f57a6eb7da 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -110,12 +110,10 @@ function boardGenerator(n) {
const name = `board${id}`;
return {
- node: {
- id,
- name,
- weight: 0,
- __typename: 'Board',
- },
+ id,
+ name,
+ weight: 0,
+ __typename: 'Board',
};
});
}
@@ -127,7 +125,7 @@ export const mockSmallProjectAllBoardsResponse = {
data: {
project: {
id: 'gid://gitlab/Project/114',
- boards: { edges: boardGenerator(3) },
+ boards: { nodes: boardGenerator(3) },
__typename: 'Project',
},
},
@@ -137,7 +135,7 @@ export const mockEmptyProjectRecentBoardsResponse = {
data: {
project: {
id: 'gid://gitlab/Project/114',
- recentIssueBoards: { edges: [] },
+ recentIssueBoards: { nodes: [] },
__typename: 'Project',
},
},
@@ -147,7 +145,7 @@ export const mockGroupAllBoardsResponse = {
data: {
group: {
id: 'gid://gitlab/Group/114',
- boards: { edges: boards },
+ boards: { nodes: boards },
__typename: 'Group',
},
},
@@ -157,7 +155,7 @@ export const mockProjectAllBoardsResponse = {
data: {
project: {
id: 'gid://gitlab/Project/1',
- boards: { edges: boards },
+ boards: { nodes: boards },
__typename: 'Project',
},
},
@@ -167,7 +165,7 @@ export const mockGroupRecentBoardsResponse = {
data: {
group: {
id: 'gid://gitlab/Group/114',
- recentIssueBoards: { edges: recentIssueBoards },
+ recentIssueBoards: { nodes: recentIssueBoards },
__typename: 'Group',
},
},
@@ -177,7 +175,7 @@ export const mockProjectRecentBoardsResponse = {
data: {
project: {
id: 'gid://gitlab/Project/1',
- recentIssueBoards: { edges: recentIssueBoards },
+ recentIssueBoards: { nodes: recentIssueBoards },
__typename: 'Project',
},
},
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index c2eafa5f51e..0a0a82c12c1 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -914,3 +914,10 @@ export const k8sNamespacesMock = [
{ metadata: { name: 'default' } },
{ metadata: { name: 'agent' } },
];
+
+export const fluxKustomizationsMock = [
+ {
+ status: 'True',
+ type: 'Ready',
+ },
+];
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index be210ed619e..20e96d771f3 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -2,7 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import { CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_UNAUTHORIZED,
+} from '~/lib/utils/http_status';
import { resolvers } from '~/environments/graphql/resolvers';
import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql';
import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql';
@@ -22,6 +26,7 @@ import {
k8sPodsMock,
k8sServicesMock,
k8sNamespacesMock,
+ fluxKustomizationsMock,
} from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
@@ -39,6 +44,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
},
};
const namespace = 'default';
+ const environmentName = 'my-environment';
beforeEach(() => {
mockResolvers = resolvers(ENDPOINT);
@@ -365,6 +371,74 @@ describe('~/frontend/environments/graphql/resolvers', () => {
},
);
});
+ describe('fluxKustomizationStatus', () => {
+ const endpoint = `${configuration.basePath}/apis/kustomize.toolkit.fluxcd.io/v1beta1/namespaces/${namespace}/kustomizations/${environmentName}`;
+
+ it('should request Flux Kustomizations via the Kubernetes API', async () => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxKustomizationStatus = await mockResolvers.Query.fluxKustomizationStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ });
+
+ expect(fluxKustomizationStatus).toEqual(fluxKustomizationsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ const apiError = 'Invalid credentials';
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.base })
+ .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
+
+ const fluxKustomizationsError = mockResolvers.Query.fluxKustomizationStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ });
+
+ await expect(fluxKustomizationsError).rejects.toThrow(apiError);
+ });
+ });
+
+ describe('fluxHelmReleaseStatus', () => {
+ const endpoint = `${configuration.basePath}/apis/helm.toolkit.fluxcd.io/v2beta1/namespaces/${namespace}/helmreleases/${environmentName}`;
+
+ it('should request Flux Helm Releases via the Kubernetes API', async () => {
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
+ .reply(HTTP_STATUS_OK, {
+ status: { conditions: fluxKustomizationsMock },
+ });
+
+ const fluxHelmReleaseStatus = await mockResolvers.Query.fluxHelmReleaseStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ });
+
+ expect(fluxHelmReleaseStatus).toEqual(fluxKustomizationsMock);
+ });
+ it('should throw an error if the API call fails', async () => {
+ const apiError = 'Invalid credentials';
+ mock
+ .onGet(endpoint, { withCredentials: true, headers: configuration.base })
+ .reply(HTTP_STATUS_UNAUTHORIZED, { message: apiError });
+
+ const fluxHelmReleasesError = mockResolvers.Query.fluxHelmReleaseStatus(null, {
+ configuration,
+ namespace,
+ environmentName,
+ });
+
+ await expect(fluxHelmReleasesError).rejects.toThrow(apiError);
+ });
+ });
+
describe('stopEnvironmentREST', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
index 1c7ace00f48..b088fce7e9e 100644
--- a/spec/frontend/environments/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -6,12 +6,13 @@ import KubernetesAgentInfo from '~/environments/components/kubernetes_agent_info
import KubernetesPods from '~/environments/components/kubernetes_pods.vue';
import KubernetesTabs from '~/environments/components/kubernetes_tabs.vue';
import KubernetesStatusBar from '~/environments/components/kubernetes_status_bar.vue';
-import { agent, kubernetesNamespace } from './graphql/mock_data';
+import { agent, kubernetesNamespace, resolvedEnvironment } from './graphql/mock_data';
import { mockKasTunnelUrl } from './mock_data';
const propsData = {
clusterAgent: agent,
namespace: kubernetesNamespace,
+ environmentName: resolvedEnvironment.name,
};
const provide = {
@@ -110,7 +111,12 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
});
it('renders kubernetes status bar', () => {
- expect(findKubernetesStatusBar().exists()).toBe(true);
+ expect(findKubernetesStatusBar().props()).toEqual({
+ clusterHealthStatus: 'success',
+ configuration,
+ namespace: kubernetesNamespace,
+ environmentName: resolvedEnvironment.name,
+ });
});
});
diff --git a/spec/frontend/environments/kubernetes_status_bar_spec.js b/spec/frontend/environments/kubernetes_status_bar_spec.js
index 2ebb30e2766..ad32818232a 100644
--- a/spec/frontend/environments/kubernetes_status_bar_spec.js
+++ b/spec/frontend/environments/kubernetes_status_bar_spec.js
@@ -1,20 +1,58 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon, GlBadge } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import KubernetesStatusBar from '~/environments/components/kubernetes_status_bar.vue';
import {
CLUSTER_STATUS_HEALTHY_TEXT,
CLUSTER_STATUS_UNHEALTHY_TEXT,
+ SYNC_STATUS_BADGES,
} from '~/environments/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { s__ } from '~/locale';
+import { mockKasTunnelUrl } from './mock_data';
+
+Vue.use(VueApollo);
+
+const configuration = {
+ basePath: mockKasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: { 'GitLab-Agent-Id': '1' },
+ withCredentials: true,
+ },
+};
+const environmentName = 'environment_name';
describe('~/environments/components/kubernetes_status_bar.vue', () => {
let wrapper;
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findHealthBadge = () => wrapper.findComponent(GlBadge);
+ const findHealthBadge = () => wrapper.findByTestId('health-badge');
+ const findSyncBadge = () => wrapper.findByTestId('sync-badge');
+
+ const fluxKustomizationStatusQuery = jest.fn().mockReturnValue([]);
+ const fluxHelmReleaseStatusQuery = jest.fn().mockReturnValue([]);
+
+ const createApolloProvider = () => {
+ const mockResolvers = {
+ Query: {
+ fluxKustomizationStatus: fluxKustomizationStatusQuery,
+ fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
- const createWrapper = ({ clusterHealthStatus = '' } = {}) => {
- wrapper = shallowMount(KubernetesStatusBar, {
- propsData: { clusterHealthStatus },
+ const createWrapper = ({
+ apolloProvider = createApolloProvider(),
+ clusterHealthStatus = '',
+ namespace = '',
+ } = {}) => {
+ wrapper = shallowMountExtended(KubernetesStatusBar, {
+ propsData: { clusterHealthStatus, configuration, environmentName, namespace },
+ apolloProvider,
});
};
@@ -39,4 +77,96 @@ describe('~/environments/components/kubernetes_status_bar.vue', () => {
},
);
});
+
+ describe('sync badge', () => {
+ describe('when no namespace is provided', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it("doesn't request Kustomizations and HelmReleases", () => {
+ expect(fluxKustomizationStatusQuery).not.toHaveBeenCalled();
+ expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
+ });
+
+ it('renders sync status as Unavailable', () => {
+ expect(findSyncBadge().text()).toBe(s__('Deployment|Unavailable'));
+ });
+ });
+
+ describe('when namespace is provided', () => {
+ describe('with no Flux resources found', () => {
+ beforeEach(() => {
+ createWrapper({ namespace: 'my-namespace' });
+ });
+
+ it('requests Kustomizations', () => {
+ expect(fluxKustomizationStatusQuery).toHaveBeenCalled();
+ });
+
+ it('requests HelmReleases when there were no Kustomizations found', async () => {
+ await waitForPromises();
+
+ expect(fluxHelmReleaseStatusQuery).toHaveBeenCalled();
+ });
+
+ it('renders sync status as Unavailable when no Kustomizations and HelmReleases found', async () => {
+ await waitForPromises();
+
+ expect(findSyncBadge().text()).toBe(s__('Deployment|Unavailable'));
+ });
+ });
+
+ describe('with Flux Kustomizations available', () => {
+ const createApolloProviderWithKustomizations = ({
+ result = { status: 'True', type: 'Ready' },
+ } = {}) => {
+ const mockResolvers = {
+ Query: {
+ fluxKustomizationStatus: jest.fn().mockReturnValue([result]),
+ fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
+ },
+ };
+
+ return createMockApollo([], mockResolvers);
+ };
+
+ it("doesn't request HelmReleases when the Kustomizations were found", async () => {
+ createWrapper({
+ apolloProvider: createApolloProviderWithKustomizations(),
+ namespace: 'my-namespace',
+ });
+ await waitForPromises();
+
+ expect(fluxHelmReleaseStatusQuery).not.toHaveBeenCalled();
+ });
+
+ it.each`
+ status | type | badgeType
+ ${'True'} | ${'Stalled'} | ${'stalled'}
+ ${'True'} | ${'Reconciling'} | ${'reconciling'}
+ ${'True'} | ${'Ready'} | ${'reconciled'}
+ ${'False'} | ${'Ready'} | ${'failed'}
+ ${'True'} | ${'Unknown'} | ${'unknown'}
+ `(
+ 'renders $badgeType when status is $status and type is $type',
+ async ({ status, type, badgeType }) => {
+ createWrapper({
+ apolloProvider: createApolloProviderWithKustomizations({ result: { status, type } }),
+ namespace: 'my-namespace',
+ });
+ await waitForPromises();
+
+ const badge = SYNC_STATUS_BADGES[badgeType];
+
+ expect(findSyncBadge().text()).toBe(badge.text);
+ expect(findSyncBadge().props()).toMatchObject({
+ icon: badge.icon,
+ variant: badge.variant,
+ });
+ },
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index 387bc31c9aa..2f319855bca 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -534,7 +534,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
describe('kubernetes overview', () => {
- it('should request agent data when the environment is visible if the feature flag is enabled', async () => {
+ it('should request agent data when the environment is visible', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
apolloProvider: createApolloProvider(agent),
@@ -578,6 +578,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
expect(findKubernetesOverview().props()).toMatchObject({
clusterAgent: agent,
+ environmentName: resolvedEnvironment.name,
});
});
@@ -595,8 +596,9 @@ describe('~/environments/components/new_environment_item.vue', () => {
await expandCollapsedSection();
await waitForPromises();
- expect(findKubernetesOverview().props()).toMatchObject({
+ expect(findKubernetesOverview().props()).toEqual({
clusterAgent: agent,
+ environmentName: resolvedEnvironment.name,
namespace: 'default',
});
});
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index 5daa21fd618..b39b9a62661 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -1,5 +1,6 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { s__, sprintf } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
@@ -17,6 +18,9 @@ const RUNNER_ENABLED_VALUE = 'enabled';
const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable';
const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_and_overridable';
+const mockParentName = 'My group';
+const mockParentSettingsPath = '/groups/my-group/-/settings/ci_cd';
+
describe('group_settings/components/shared_runners_form', () => {
let wrapper;
@@ -27,20 +31,19 @@ describe('group_settings/components/shared_runners_form', () => {
groupName: GROUP_NAME,
groupIsEmpty: false,
sharedRunnersSetting: RUNNER_ENABLED_VALUE,
- parentSharedRunnersSetting: null,
+
runnerEnabledValue: RUNNER_ENABLED_VALUE,
runnerDisabledValue: RUNNER_DISABLED_VALUE,
runnerAllowOverrideValue: RUNNER_ALLOW_OVERRIDE_VALUE,
...provide,
},
+ stubs: {
+ GlSprintf,
+ },
});
};
- const findAlert = (variant) =>
- wrapper
- .findAllComponents(GlAlert)
- .filter((w) => w.props('variant') === variant)
- .at(0);
+ const findAlert = () => wrapper.findComponent(GlAlert);
const findSharedRunnersToggle = () => wrapper.findByTestId('shared-runners-toggle');
const findOverrideToggle = () => wrapper.findByTestId('override-runners-toggle');
const getSharedRunnersSetting = () => {
@@ -86,17 +89,37 @@ describe('group_settings/components/shared_runners_form', () => {
});
});
- describe('When parent group disabled shared runners', () => {
- it('toggles are disabled', () => {
+ describe.each`
+ provide | case | isParentLinkExpected
+ ${{ parentName: mockParentName, parentSettingsPath: mockParentSettingsPath }} | ${'can configure parent'} | ${true}
+ ${{}} | ${'cannot configure parent'} | ${false}
+ `('When parent group disabled shared runners and $case', ({ provide, isParentLinkExpected }) => {
+ beforeEach(() => {
createComponent({
sharedRunnersSetting: RUNNER_DISABLED_VALUE,
parentSharedRunnersSetting: RUNNER_DISABLED_VALUE,
+ ...provide,
});
-
- expect(findSharedRunnersToggle().props('disabled')).toBe(true);
- expect(findOverrideToggle().props('disabled')).toBe(true);
- expect(findAlert('warning').exists()).toBe(true);
});
+
+ it.each([findSharedRunnersToggle, findOverrideToggle])(
+ 'toggle %# is disabled',
+ (findToggle) => {
+ expect(findToggle().props('disabled')).toBe(true);
+ expect(findToggle().text()).toContain(s__('Runners|Shared runners are disabled.'));
+
+ if (isParentLinkExpected) {
+ expect(findToggle().text()).toContain(
+ sprintf(s__('Runners|Go to %{groupLink} to enable them.'), {
+ groupLink: mockParentName,
+ }),
+ );
+ const link = findToggle().findComponent(GlLink);
+ expect(link.text()).toBe(mockParentName);
+ expect(link.attributes('href')).toBe(mockParentSettingsPath);
+ }
+ },
+ );
});
describe('loading state', () => {
@@ -240,7 +263,7 @@ describe('group_settings/components/shared_runners_form', () => {
});
it('error should be shown', () => {
- expect(findAlert('danger').text()).toBe(message);
+ expect(findAlert().text()).toBe(message);
});
});
});
diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js
index 1ca58053e68..d82d65e3549 100644
--- a/spec/frontend/token_access/inbound_token_access_spec.js
+++ b/spec/frontend/token_access/inbound_token_access_spec.js
@@ -21,6 +21,7 @@ import {
} from './mock_data';
const projectPath = 'root/my-repo';
+const testProjectPath = 'root/test';
const message = 'An error occurred';
const error = new Error(message);
@@ -53,10 +54,11 @@ describe('TokenAccess component', () => {
const findToggle = () => wrapper.findComponent(GlToggle);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' });
+ const findAddProjectBtn = () => wrapper.findByTestId('add-project-btn');
const findCancelBtn = () => wrapper.findByRole('button', { name: 'Cancel' });
const findProjectInput = () => wrapper.findComponent(GlFormInput);
const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
+ const findToggleFormBtn = () => wrapper.findByTestId('toggle-form-btn');
const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert);
const createMockApolloProvider = (requestHandlers) => {
@@ -69,11 +71,6 @@ describe('TokenAccess component', () => {
fullPath: projectPath,
},
apolloProvider: createMockApolloProvider(requestHandlers),
- data() {
- return {
- targetProjectPath: 'root/test',
- };
- },
});
};
@@ -222,11 +219,13 @@ describe('TokenAccess component', () => {
await waitForPromises();
+ await findToggleFormBtn().trigger('click');
+ await findProjectInput().vm.$emit('input', testProjectPath);
findAddProjectBtn().trigger('click');
expect(inboundAddProjectSuccessResponseHandler).toHaveBeenCalledWith({
projectPath,
- targetProjectPath: 'root/test',
+ targetProjectPath: testProjectPath,
});
});
@@ -242,6 +241,8 @@ describe('TokenAccess component', () => {
await waitForPromises();
+ await findToggleFormBtn().trigger('click');
+ await findProjectInput().vm.$emit('input', testProjectPath);
findAddProjectBtn().trigger('click');
await waitForPromises();
@@ -249,7 +250,7 @@ describe('TokenAccess component', () => {
expect(createAlert).toHaveBeenCalledWith({ message });
});
- it('clicking cancel clears target path', async () => {
+ it('clicking cancel hides the form and clears the target path', async () => {
createComponent(
[
[inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
@@ -260,10 +261,18 @@ describe('TokenAccess component', () => {
await waitForPromises();
- expect(findProjectInput().element.value).toBe('root/test');
+ await findToggleFormBtn().trigger('click');
+
+ expect(findProjectInput().exists()).toBe(true);
+
+ await findProjectInput().vm.$emit('input', testProjectPath);
await findCancelBtn().trigger('click');
+ expect(findProjectInput().exists()).toBe(false);
+
+ await findToggleFormBtn().trigger('click');
+
expect(findProjectInput().element.value).toBe('');
});
});
diff --git a/spec/frontend/token_access/outbound_token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js
index f9eb201eb5c..c5224d5d942 100644
--- a/spec/frontend/token_access/outbound_token_access_spec.js
+++ b/spec/frontend/token_access/outbound_token_access_spec.js
@@ -38,9 +38,9 @@ describe('TokenAccess component', () => {
const findToggle = () => wrapper.findComponent(GlToggle);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' });
const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
const findDeprecationAlert = () => wrapper.findByTestId('deprecation-alert');
- const findProjectPathInput = () => wrapper.findByTestId('project-path-input');
const createMockApolloProvider = (requestHandlers) => {
return createMockApollo(requestHandlers);
@@ -247,7 +247,7 @@ describe('TokenAccess component', () => {
});
describe('adding a new project', () => {
- it('disables the input to add new projects', async () => {
+ it('disables the button for adding new projects', async () => {
createComponent(
[
[getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
@@ -260,7 +260,7 @@ describe('TokenAccess component', () => {
await waitForPromises();
- expect(findProjectPathInput().attributes('disabled')).toBe('disabled');
+ expect(findAddProjectBtn().attributes('disabled')).toBe('disabled');
});
});
});
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
index dc593228388..4f47d7c936f 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -115,12 +115,19 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do
}
end
+ before do
+ allow(helper).to receive(:can?).with(user, :admin_group, parent).and_return(true)
+ end
+
it 'returns group data for top level group' do
result = {
group_id: parent.id,
group_name: parent.name,
group_is_empty: 'false',
shared_runners_setting: Namespace::SR_ENABLED,
+
+ parent_name: nil,
+ parent_settings_path: nil,
parent_shared_runners_setting: nil
}.merge(runner_constants)
@@ -133,7 +140,27 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do
group_name: group.name,
group_is_empty: 'true',
shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
- parent_shared_runners_setting: Namespace::SR_ENABLED
+
+ parent_shared_runners_setting: Namespace::SR_ENABLED,
+ parent_name: parent.name,
+ parent_settings_path: group_settings_ci_cd_path(group.parent, anchor: 'js-runner-settings')
+ }.merge(runner_constants)
+
+ expect(helper.group_shared_runners_settings_data(group)).to eq result
+ end
+
+ it 'returns groups data for child group with no access to parent' do
+ allow(helper).to receive(:can?).with(user, :admin_group, parent).and_return(false)
+
+ result = {
+ group_id: group.id,
+ group_name: group.name,
+ group_is_empty: 'true',
+ shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE,
+
+ parent_shared_runners_setting: Namespace::SR_ENABLED,
+ parent_name: nil,
+ parent_settings_path: nil
}.merge(runner_constants)
expect(helper.group_shared_runners_settings_data(group)).to eq result
@@ -145,7 +172,10 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do
group_name: group_with_project.name,
group_is_empty: 'false',
shared_runners_setting: Namespace::SR_ENABLED,
- parent_shared_runners_setting: Namespace::SR_ENABLED
+
+ parent_shared_runners_setting: Namespace::SR_ENABLED,
+ parent_name: parent.name,
+ parent_settings_path: group_settings_ci_cd_path(group.parent, anchor: 'js-runner-settings')
}.merge(runner_constants)
expect(helper.group_shared_runners_settings_data(group_with_project)).to eq result
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 1de19faf063..c4b8f4f0145 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -279,6 +279,29 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe '.for_status' do
+ subject { described_class.for_status(status) }
+
+ let_it_be(:pipeline1) { create(:ci_pipeline, name: 'Build pipeline', status: :created) }
+ let_it_be(:pipeline2) { create(:ci_pipeline, name: 'Chatops pipeline', status: :failed) }
+
+ context 'when status exists' do
+ let(:status) { :created }
+
+ it 'performs exact compare' do
+ is_expected.to contain_exactly(pipeline1)
+ end
+ end
+
+ context 'when status does not exist' do
+ let(:status) { :pending }
+
+ it 'returns empty' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe '.created_after' do
let_it_be(:old_pipeline) { create(:ci_pipeline, created_at: 1.week.ago) }
let_it_be(:pipeline) { create(:ci_pipeline) }
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index a6766035c1e..e557990c7e9 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -180,8 +180,8 @@ RSpec.describe ProjectTeam, feature_category: :groups_and_projects do
subject(:import) { target_project.team.import(source_project, current_user) }
- it 'matches the imported members', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/419394' do
- is_expected.to match(imported_members)
+ it 'matches the imported members' do
+ is_expected.to match_array(imported_members)
end
it 'target project includes source member with the same access' do
diff --git a/spec/support/fast_quarantine.rb b/spec/support/fast_quarantine.rb
index b5ed1a2aa96..9732a287cb2 100644
--- a/spec/support/fast_quarantine.rb
+++ b/spec/support/fast_quarantine.rb
@@ -7,10 +7,9 @@ return if ENV['CI_MERGE_REQUEST_LABELS'].to_s.include?('pipeline:run-flaky-tests
require_relative '../../tooling/lib/tooling/fast_quarantine'
RSpec.configure do |config|
- fast_quarantine_local_path = ENV.fetch('RSPEC_FAST_QUARANTINE_LOCAL_PATH', 'rspec/fast_quarantine-gitlab.txt')
fast_quarantine_path = ENV.fetch(
'RSPEC_FAST_QUARANTINE_PATH',
- File.expand_path("../../#{fast_quarantine_local_path}", __dir__)
+ File.expand_path("../../rspec/fast_quarantine-gitlab.txt", __dir__)
)
fast_quarantine = Tooling::FastQuarantine.new(fast_quarantine_path: fast_quarantine_path)
skipped_examples = []
@@ -28,10 +27,12 @@ RSpec.configure do |config|
next if skipped_examples.empty?
skipped_tests_report_path = ENV.fetch(
- 'SKIPPED_TESTS_REPORT_PATH',
+ 'RSPEC_SKIPPED_TESTS_REPORT_PATH',
File.expand_path("../../rspec/flaky/skipped_tests.txt", __dir__)
)
+ next warn("#{skipped_tests_report_path} doesn't exist!") unless File.exist?(skipped_tests_report_path.to_s)
+
File.write(skipped_tests_report_path, "#{ENV.fetch('CI_JOB_URL', 'local-run')}\n#{skipped_examples.join("\n")}\n\n")
end
end