Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-08-07 18:10:22 +00:00
parent 33c86930e0
commit 4d18bba787
61 changed files with 1265 additions and 441 deletions

View File

@ -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/

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -64,6 +64,10 @@
rules:
- when: always
.rules:download-fast-quarantine-report:
rules:
- when: always
# ------------------------------------------
# Test
# ------------------------------------------

View File

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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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.
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/>
* **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
## Pre-Sprint Preparation | Due Date: `Add Date`
| 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 wes 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. |
- [ ] 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.
### 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 theyve 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. |
## 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.
### 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: Dont add any unnecessary details or ideas that arent 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. |
| 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. |
### 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. |
#### Post-Day Facilitator Checklist:
- [ ] Answer any questions that come up during the Q&A period.
- [ ] Send a homework reminder in the Slack channel.
### Day 5
| Activity | Duration | Tool | Description |
|---|---|---|---|
| Prototype testing with 5 users | ~45 mins | Figma or code/Zoom | • Test the prototype with users. |
## 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 Wes (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, well 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 youd 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 youll 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/> • Its 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.

View File

@ -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,
});
},
},
};

View File

@ -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,
},
});
},

View File

@ -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,
},
});
}
},
},

View File

@ -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,
});

View File

@ -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: {

View File

@ -2,11 +2,9 @@ query group_boards($fullPath: ID!) {
group(fullPath: $fullPath) {
id
boards {
edges {
node {
id
name
}
nodes {
id
name
}
}
}

View File

@ -2,11 +2,9 @@ query group_recent_boards($fullPath: ID!) {
group(fullPath: $fullPath) {
id
recentIssueBoards {
edges {
node {
id
name
}
nodes {
id
name
}
}
}

View File

@ -2,11 +2,9 @@ query project_boards($fullPath: ID!) {
project(fullPath: $fullPath) {
id
boards {
edges {
node {
id
name
}
nodes {
id
name
}
}
}

View File

@ -2,11 +2,9 @@ query project_recent_boards($fullPath: ID!) {
project(fullPath: $fullPath) {
id
recentIssueBoards {
edges {
node {
id
name
}
nodes {
id
name
}
}
}

View File

@ -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">

View File

@ -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>

View File

@ -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

View File

@ -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';

View File

@ -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,
});

View File

@ -0,0 +1,14 @@
query getFluxHelmReleaseStatusQuery(
$configuration: LocalConfiguration
$namespace: String
$environmentName: String
) {
fluxHelmReleaseStatus(
configuration: $configuration
namespace: $namespace
environmentName: $environmentName
) @client {
status
type
}
}

View File

@ -0,0 +1,14 @@
query getFluxHelmKustomizationStatusQuery(
$configuration: LocalConfiguration
$namespace: String
$environmentName: String
) {
fluxKustomizationStatus(
configuration: $configuration
namespace: $namespace
environmentName: $environmentName
) @client {
status
type
}
}

View File

@ -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 }) {

View File

@ -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 {

View File

@ -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>

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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)

View File

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

View File

@ -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?

View File

@ -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'

View File

@ -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).

View File

@ -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

View File

@ -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

View File

@ -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)
Below is an example on how to convert a job using this pattern:
For this scenario, you have to:
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!"
.base-job:
before_script:
echo "Hello from .base-job"
# 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!"
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

View File

@ -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 |
|:-------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|

View File

@ -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 ""

View File

@ -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

View File

@ -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?

View File

@ -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}")/"
echoinfo "RSPEC_RETRIED_TESTS_REPORT_PATH: ${RSPEC_RETRIED_TESTS_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}"
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
}

View File

@ -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);

View File

@ -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('');
}
});
});

View File

@ -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',
},
},

View File

@ -914,3 +914,10 @@ export const k8sNamespacesMock = [
{ metadata: { name: 'default' } },
{ metadata: { name: 'agent' } },
];
export const fluxKustomizationsMock = [
{
status: 'True',
type: 'Ready',
},
];

View File

@ -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);

View File

@ -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,
});
});
});

View File

@ -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 createWrapper = ({ clusterHealthStatus = '' } = {}) => {
wrapper = shallowMount(KubernetesStatusBar, {
propsData: { clusterHealthStatus },
const fluxKustomizationStatusQuery = jest.fn().mockReturnValue([]);
const fluxHelmReleaseStatusQuery = jest.fn().mockReturnValue([]);
const createApolloProvider = () => {
const mockResolvers = {
Query: {
fluxKustomizationStatus: fluxKustomizationStatusQuery,
fluxHelmReleaseStatus: fluxHelmReleaseStatusQuery,
},
};
return createMockApollo([], mockResolvers);
};
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,
});
},
);
});
});
});
});

View File

@ -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',
});
});

View File

@ -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);
});
});
});

View File

@ -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('');
});
});

View File

@ -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');
});
});
});

View File

@ -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

View File

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

View File

@ -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

View File

@ -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