speed up ci builds from 15 to <7 minutes (#1615)

This PR cuts GitHub Action build times from 14-15 minutes, down to just
under 7 minutes. It does this by:
- caching `grafana-plugins/node_modules` and `pip` dependencies based on
their respective dependency files (eg. `requirements.txt` &
`yarn.lock`). This step alone saves ~3 minutes.
- get rid of the "build-engine-docker-image" and
"backend-integration-tests" jobs in the old "Integration Tests"
workflow. This was split out this way so that we could build the backend
docker image once, upload the artifact, and then reuse it across the
backend and e2e tests. We no longer need these backend integration tests
because we are testing the same thing in the e2e tests. This saves ~45
seconds of having to upload the image artifact.
- few improvements within the integration tests themselves:
- move plugin configuration to the `globalSetup.ts`. This means that
every test does not need to check if the plugin has been configured
because it is done once before all the tests are run.
- cache the plugin frontend build. If your commit doesn't change
anything to `grafana-plugin/src` or `grafana-plugin/yarn.lock` it should
be safe to reuse a previously built/cached version of the plugin
frontend. This saves ~3 minutes
- cache playwright binaries/dependencies. Only re-install them if the
version of `@playwright/test` in `grafana-plugin/yarn.lock` changes.
This saves ~3 minutes.
  
**Other things to mention**
Once we refactor the `GSelect` component to not call the `onChange`
callback on every keyDown event (#1628), this should allow us to
parallelize the integration tests, and cut the time required to execute
the tests themselves in half
This commit is contained in:
Joey Orlando 2023-03-27 18:07:19 +02:00 committed by GitHub
parent f44a4f40a3
commit 23cd736c30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 562 additions and 624 deletions

View file

@ -1,211 +0,0 @@
name: ci
on:
push:
branches:
- main
- dev
pull_request:
# You can use the merge_group event to trigger your GitHub Actions workflow when
# a pull request is added to a merge queue
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions
merge_group:
jobs:
lint:
runs-on: ubuntu-latest
container: python:3.9
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
- name: Build
run: |
pip install $(grep "pre-commit" engine/requirements.txt)
npm install -g yarn
cd grafana-plugin/
yarn --network-timeout 500000
yarn build
# pre-commit uses git, which is not working in the action without this workaround
# see https://github.com/actions/runner-images/issues/6775
- run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Lint All
run: |
pre-commit run --all-files
test-frontend:
runs-on: ubuntu-latest
container: python:3.9
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
- name: Unit Testing Frontend
run: |
pip install $(grep "pre-commit" engine/requirements.txt)
npm install -g yarn
cd grafana-plugin/
yarn --network-timeout 500000
yarn test
test-technical-documentation:
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: "actions/checkout@v3"
- name: "Build website"
# -e HUGO_REFLINKSERRORLEVEL=ERROR prevents merging broken refs with the downside
# that no refs to external content can be used as these refs will not resolve in the
# docs-base image.
run: |
docker run -v ${PWD}/docs/sources:/hugo/content/docs/oncall/latest -e HUGO_REFLINKSERRORLEVEL=ERROR --rm grafana/docs-base:latest /bin/bash -c 'make hugo'
lint-migrations-backend-mysql-rabbitmq:
name: "Lint migrations"
runs-on: ubuntu-latest
container: python:3.9
env:
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
mysql_test:
image: mysql:5.7.25
env:
MYSQL_DATABASE: oncall_local_dev
MYSQL_ROOT_PASSWORD: local_dev_pwd
steps:
- uses: actions/checkout@v3
- name: Lint migrations
run: |
cd engine/
pip install -r requirements.txt
python manage.py lintmigrations
unit-test-backend-mysql-rabbitmq:
name: "Backend Tests: MySQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
container: python:3.9
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
mysql_test:
image: mysql:5.7.25
env:
MYSQL_DATABASE: oncall_local_dev
MYSQL_ROOT_PASSWORD: local_dev_pwd
steps:
- uses: actions/checkout@v2
- name: Unit Test Backend
run: |
apt-get update && apt-get install -y netcat
cd engine/
pip install -r requirements.txt
./wait_for_test_mysql_start.sh && ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x
unit-test-backend-postgresql-rabbitmq:
name: "Backend Tests: PostgreSQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
container: python:3.9
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: postgresql
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
postgresql_test:
image: postgres:14.4
env:
POSTGRES_DB: oncall_local_dev
POSTGRES_PASSWORD: local_dev_pwd
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Unit Test Backend
run: |
cd engine/
pip install -r requirements.txt
ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x
unit-test-backend-sqlite-redis:
name: "Backend Tests: SQLite + Redis (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
container: python:3.9
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: sqlite3
BROKER_TYPE: redis
REDIS_URI: redis://redis_test:6379
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
redis_test:
image: redis:7.0.5
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Unit Test Backend
run: |
apt-get update && apt-get install -y netcat
cd engine/
pip install -r requirements.txt
ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x
unit-test-pd-migrator:
runs-on: ubuntu-latest
container: python:3.9
steps:
- uses: actions/checkout@v2
- name: Unit Test PD Migrator
run: |
cd tools/pagerduty-migrator
pip install -r requirements.txt
pytest -x
docker-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Test docker build (no push)
id: docker_build
uses: docker/build-push-action@v2
with:
context: ./engine
file: ./engine/Dockerfile
push: false
target: prod
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}

View file

@ -1,4 +1,4 @@
name: helm-release
name: Helm Release
on:
push:

View file

@ -1,294 +0,0 @@
name: Integration Tests
on:
pull_request:
# You can use the merge_group event to trigger your GitHub Actions workflow when
# a pull request is added to a merge queue
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions
merge_group:
# TODO: ideally we would be able to have one CI job which spins up the kind cluster and does the helm release
# then we could have the UI and backend integration tests dependent on this job and not have to each
# independently spin up the cluster. This doesn't seem to be supported however
# https://github.com/docker/build-push-action/issues/225
#
# Probably one way to get around this would be to deploy the helm release to a sandbox k8s cluster somewhere? and reference
# that in the various integration test jobs
jobs:
build-engine-docker-image:
name: Build engine Docker image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx # We need this step for docker caching
uses: docker/setup-buildx-action@v2
- name: Build docker image locally # using github actions docker cache
uses: docker/build-push-action@v2
with:
context: ./engine
file: ./engine/Dockerfile
push: false
load: true
tags: oncall/engine:latest
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=docker,dest=/tmp/oncall-engine.tar
# https://github.com/docker/build-push-action/issues/225#issuecomment-727639184
- name: Persist engine Docker image between jobs
uses: actions/upload-artifact@v2
with:
name: oncall-engine
path: /tmp/oncall-engine.tar
backend-integration-tests:
name: Backend Integration Tests
needs: build-engine-docker-image
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Download engine Docker image
uses: actions/download-artifact@v2
with:
name: oncall-engine
path: /tmp
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.3.0
with:
config: ./helm/kind.yml
- name: Load image on the nodes of the cluster
run: kind load image-archive --name=chart-testing /tmp/oncall-engine.tar
- name: Install helm chart
run: |
helm install test-release \
--values ./simple.yml \
--values ./values-local-image.yml \
./oncall
working-directory: helm
- name: Await k8s pods and other resources up
uses: jupyterhub/action-k8s-await-workloads@v1
with:
workloads: "" # all
namespace: "" # default
timeout: 300
max-restarts: -1
# GitHub Action reference: https://github.com/jupyterhub/action-k8s-namespace-report
- name: Kubernetes namespace report
uses: jupyterhub/action-k8s-namespace-report@v1
if: always()
- name: Bootstrap organization and integration
run: |
export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=oncall,app.kubernetes.io/instance=test-release,app.kubernetes.io/component=engine" -o jsonpath="{.items[0].metadata.name}")
export ONCALL_INTEGRATION_URL=http://localhost:30001$(kubectl exec -it $POD_NAME -- bash -c "python manage.py setup_end_to_end_test --bootstrap_integration")
echo "ONCALL_INTEGRATION_URL=$ONCALL_INTEGRATION_URL" >> $GITHUB_ENV
- name: Send an alert to the integration
run: |
echo $ONCALL_INTEGRATION_URL
export TEST_ID=test-0
echo "TEST_ID=$TEST_ID" >> $GITHUB_ENV
curl -X POST "$ONCALL_INTEGRATION_URL" \
-H 'Content-Type: Application/json' \
-d '{
"alert_uid": "08d6891a-835c-e661-39fa-96b6a9e26552",
"title": "'"$TEST_ID"'",
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg",
"state": "alerting",
"link_to_upstream_details": "https://en.wikipedia.org/wiki/Downtime",
"message": "Smth happened. Oh no!"
}'
- name: Await 1 alert group and 1 alert created during the test (timeout 30 seconds)
run: |
export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=oncall,app.kubernetes.io/instance=test-release,app.kubernetes.io/component=engine" -o jsonpath="{.items[0].metadata.name}")
tries=30
while [ "$tries" -gt 0 ]; do
if kubectl exec -it $POD_NAME -c oncall -- bash -c "python manage.py setup_end_to_end_test --return_results_for_test_id $TEST_ID" | grep -q '1, 1'
then
break
fi
tries=$(( tries - 1 ))
sleep 1
done
if [ "$tries" -eq 0 ]; then
echo 'Expected "1, 1" (alert groups, alerts). They were not created in 30 seconds during this integration test. Something is broken' >&2
exit 1
fi
ui-integration-tests:
needs: build-engine-docker-image
runs-on: ubuntu-latest
name: "UI Integration Tests - Grafana: ${{ matrix.grafana-image-tag }}"
strategy:
matrix:
grafana-image-tag:
- 9.2.6
- main
- latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Download engine Docker image
uses: actions/download-artifact@v2
with:
name: oncall-engine
path: /tmp
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.3.0
with:
config: ./helm/kind.yml
- name: Load image on the nodes of the cluster
run: kind load image-archive --name=chart-testing /tmp/oncall-engine.tar
# yarn caching doesn't properly work with subdirectories hence the following two steps
# which calculate a cache key and restore the cache manually
# see this GH issue for more details https://github.com/actions/setup-node/issues/488#issue-1231822552
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
shell: bash
working-directory: ./grafana-plugin
- name: Restore yarn cache
uses: actions/cache@v3
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: yarn-cache-folder-${{ hashFiles('**/yarn.lock', '.yarnrc.yml') }}
restore-keys: |
yarn-cache-folder-
# https://stackoverflow.com/a/62244232
# --prefer-offline tells yarn to use cached downloads (in the cache directory mentioned above)
# during installation whenever possible instead of downloading from the server.
- name: Install dependencies
working-directory: ./grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline
# build the plugin frontend
- name: Build plugin frontend
working-directory: ./grafana-plugin
run: yarn build:dev
# by settings grafana.plugins to [] and configuring grafana.extraVolumeMounts we are using the locally built
# OnCall plugin rather than the latest published version
# the /oncall-plugin hostPath refers to the kind volumeMount that points to the ./grafana-plugin dir
# see ./helm/kind.yml for more details
- name: Install helm chart
run: |
helm install helm-testing \
--values ./helm/simple.yml \
--values ./helm/values-local-image.yml \
--set-json 'env=[{"name":"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED","value":"False"}]' \
--set oncall.twilio.accountSid="${{ secrets.TWILIO_ACCOUNT_SID }}" \
--set oncall.twilio.authToken="${{ secrets.TWILIO_AUTH_TOKEN }}" \
--set oncall.twilio.phoneNumber="\"${{ secrets.TWILIO_PHONE_NUMBER }}"\" \
--set oncall.twilio.verifySid="${{ secrets.TWILIO_VERIFY_SID }}" \
--set grafana.image.tag=${{ matrix.grafana-image-tag }} \
--set grafana.env.GF_SECURITY_ADMIN_USER=oncall \
--set grafana.env.GF_SECURITY_ADMIN_PASSWORD=oncall \
--set grafana.env.GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-oncall-app \
--set-json "grafana.plugins=[]" \
--set-json 'grafana.securityContext={"runAsUser": 0, "runAsGroup": 0, "fsGroup": 0}' \
--set-json 'grafana.extraVolumeMounts=[{"name":"plugins","mountPath":"/var/lib/grafana/plugins/grafana-plugin","hostPath":"/oncall-plugin","readOnly":true}]' \
./helm/oncall
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1154603556
# Figures out the version of playwright that's installed.
# The result is stored in steps.playwright-version.outputs.version
- name: Get installed Playwright version
id: playwright-version
working-directory: ./grafana-plugin
run: echo "::set-output name=version::$(yarn list --pattern @playwright/test | grep @playwright/test@ | sed 's/[^0-9.]*\([0-9.]*\).*/\1/')"
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1317670494
# Attempt to restore the correct Playwright browser binaries based on the
# currently installed version of Playwright (The browser binary versions
# may change with Playwright versions).
# Note: Playwright's cache directory is hard coded because that's what it
# says to do in the docs. There doesn't appear to be a command that prints
# it out for us.
- name: Cache Playwright binaries
uses: actions/cache@v3
id: playwright-cache
with:
path: "~/.cache/ms-playwright"
key: "${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}"
# TODO: If the Playwright browser binaries weren't able to be restored, install them
# https://github.com/microsoft/playwright/issues/7249#issuecomment-1256878540
- name: Install Playwright
# if: steps.playwright-cache.outputs.cache-hit != 'true'
working-directory: ./grafana-plugin
run: |
npx playwright install
npx playwright install-deps
# - name: Install Playwright system dependencies
# run: npx playwright install-deps
# if: steps.playwright-cache.outputs.cache-hit == 'true'
# working-directory: ./grafana-plugin
- name: Await k8s pods and other resources up
uses: jupyterhub/action-k8s-await-workloads@v1
with:
workloads: "" # all
namespace: "" # default
timeout: 300
max-restarts: -1
# GitHub Action reference: https://github.com/jupyterhub/action-k8s-namespace-report
- name: Kubernetes namespace report
uses: jupyterhub/action-k8s-namespace-report@v1
if: always()
- name: Run Integration Tests
env:
# BASE_URL represents what is accessed via a browser
BASE_URL: http://localhost:30002/grafana
# ONCALL_API_URL is what is configured in the plugin configuration form
# it is what the grafana container uses to communicate with the OnCall backend
#
# 172.17.0.1 is the docker bridge network default gateway. Requests originate in the grafana container
# hit 172.17.0.1 which proxies the request onto the host where port 30001 is the node port that is mapped
# to the OnCall API
ONCALL_API_URL: http://172.17.0.1:30001
GRAFANA_USERNAME: oncall
GRAFANA_PASSWORD: oncall
MAILSLURP_API_KEY: ${{ secrets.MAILSLURP_API_KEY }}
working-directory: ./grafana-plugin
run: yarn test:integration
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: ./grafana-plugin/playwright-report/
retention-days: 30

View file

@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Actions
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
repository: "grafana/grafana-github-actions"
path: ./actions

427
.github/workflows/linting-and-tests.yml vendored Normal file
View file

@ -0,0 +1,427 @@
name: Linting and Unit/e2e Tests
on:
push:
branches:
- main
- dev
pull_request:
# You can use the merge_group event to trigger your GitHub Actions workflow when
# a pull request is added to a merge queue
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue#triggering-merge-group-checks-with-github-actions
merge_group:
concurrency:
# Cancel any running workflow for the same branch when new commits are pushed.
# We group both by ref_name (available when CI is triggered by a push to a branch/tag)
# and head_ref (available when CI is triggered by a PR).
group: "${{ github.ref_name }}-${{ github.head_ref }}"
cancel-in-progress: true
jobs:
lint-entire-project:
name: "Lint entire project"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
# following 2 steps - need to install the frontend dependencies for the eslint/prettier/stylelint steps
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- name: Use cached frontend dependencies
id: cache-frontend-dependencies
uses: actions/cache@v3
with:
path: grafana-plugin/node_modules
key: ${{ runner.os }}-frontend-node-modules-${{ hashFiles('grafana-plugin/yarn.lock') }}
- name: Install frontend dependencies
if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true'
working-directory: grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000
- uses: pre-commit/action@v3.0.0
lint-test-and-build-frontend:
name: "Lint, test, and build frontend"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- name: Use cached frontend dependencies
id: cache-frontend-dependencies
uses: actions/cache@v3
with:
path: grafana-plugin/node_modules
key: ${{ runner.os }}-frontend-node-modules-${{ hashFiles('grafana-plugin/yarn.lock') }}
- name: Install frontend dependencies
if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true'
working-directory: grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000
- name: Build frontend (will run linter and tests)
working-directory: grafana-plugin
run: yarn build
test-technical-documentation:
name: "Test technical documentation"
runs-on: ubuntu-latest
steps:
- name: "Check out code"
uses: "actions/checkout@v3"
- name: "Build website"
# -e HUGO_REFLINKSERRORLEVEL=ERROR prevents merging broken refs with the downside
# that no refs to external content can be used as these refs will not resolve in the
# docs-base image.
run: |
docker run -v ${PWD}/docs/sources:/hugo/content/docs/oncall/latest -e HUGO_REFLINKSERRORLEVEL=ERROR --rm grafana/docs-base:latest /bin/bash -c 'make hugo'
lint-migrations-backend-mysql-rabbitmq:
name: "Lint database migrations"
runs-on: ubuntu-latest
env:
DATABASE_HOST: localhost
RABBITMQ_URI: amqp://rabbitmq:rabbitmq@localhost:5672
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
ports:
- 5672:5672
mysql_test:
image: mysql:5.7.25
env:
MYSQL_DATABASE: oncall_local_dev
MYSQL_ROOT_PASSWORD: local_dev_pwd
ports:
- 3306:3306
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- name: Lint migrations
working-directory: engine
run: |
pip install -r requirements.txt
python manage.py lintmigrations
unit-test-backend-mysql-rabbitmq:
name: "Backend Tests: MySQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DJANGO_SETTINGS_MODULE: settings.ci-test
DATABASE_HOST: localhost
RABBITMQ_URI: amqp://rabbitmq:rabbitmq@localhost:5672
SLACK_CLIENT_OAUTH_ID: 1
ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }}
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
ports:
- 5672:5672
mysql_test:
image: mysql:5.7.25
env:
MYSQL_DATABASE: oncall_local_dev
MYSQL_ROOT_PASSWORD: local_dev_pwd
ports:
- 3306:3306
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- name: Unit Test Backend
working-directory: engine
run: |
apt-get update && apt-get install -y netcat
pip install -r requirements.txt
./wait_for_test_mysql_start.sh && pytest -x
unit-test-backend-postgresql-rabbitmq:
name: "Backend Tests: PostgreSQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: postgresql
DATABASE_HOST: localhost
RABBITMQ_URI: amqp://rabbitmq:rabbitmq@localhost:5672
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }}
services:
rabbit_test:
image: rabbitmq:3.7.19
env:
RABBITMQ_DEFAULT_USER: rabbitmq
RABBITMQ_DEFAULT_PASS: rabbitmq
ports:
- 5672:5672
postgresql_test:
image: postgres:14.4
env:
POSTGRES_DB: oncall_local_dev
POSTGRES_PASSWORD: local_dev_pwd
ports:
- 5432:5432
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- name: Unit Test Backend
working-directory: engine
run: |
pip install -r requirements.txt
pytest -x
unit-test-backend-sqlite-redis:
name: "Backend Tests: SQLite + Redis (RBAC enabled: ${{ matrix.rbac_enabled }})"
runs-on: ubuntu-latest
strategy:
matrix:
rbac_enabled: ["True", "False"]
env:
DATABASE_TYPE: sqlite3
BROKER_TYPE: redis
REDIS_URI: redis://localhost:6379
DJANGO_SETTINGS_MODULE: settings.ci-test
SLACK_CLIENT_OAUTH_ID: 1
ONCALL_TESTING_RBAC_ENABLED: ${{ matrix.rbac_enabled }}
services:
redis_test:
image: redis:7.0.5
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- name: Unit Test Backend
working-directory: engine
run: |
apt-get update && apt-get install -y netcat
pip install -r requirements.txt
pytest -x
unit-test-pd-migrator:
name: "Unit tests - PagerDuty Migrator"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: tools/pagerduty-migrator/requirements.txt
- name: Unit Test PD Migrator
working-directory: tools/pagerduty-migrator
run: |
pip install -r requirements.txt
pytest -x
end-to-end-tests:
runs-on: ubuntu-latest
name: "End to end tests - Grafana: ${{ matrix.grafana-image-tag }}"
strategy:
matrix:
grafana-image-tag:
- 9.2.6
- main
- latest
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.3.0
with:
config: ./helm/kind.yml
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- name: Use cached frontend dependencies
id: cache-frontend-dependencies
uses: actions/cache@v3
with:
path: grafana-plugin/node_modules
key: ${{ runner.os }}-frontend-node-modules-${{ hashFiles('grafana-plugin/yarn.lock') }}
- name: Install frontend dependencies
if: steps.cache-frontend-dependencies.outputs.cache-hit != 'true'
working-directory: grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000
- name: Use cached plugin frontend build
id: cache-plugin-frontend
uses: actions/cache@v3
with:
path: grafana-plugin/dist
key: ${{ runner.os }}-plugin-frontend-${{ hashFiles('grafana-plugin/src/**/*', 'grafana-plugin/yarn.lock') }}
- name: Build plugin frontend
if: steps.cache-plugin-frontend.outputs.cache-hit != 'true'
working-directory: grafana-plugin
run: yarn build:dev
- name: Set up Docker Buildx # We need this step for docker caching
uses: docker/setup-buildx-action@v2
- name: Build engine Docker image locally # using github actions docker cache
uses: docker/build-push-action@v2
with:
context: ./engine
file: ./engine/Dockerfile
push: false
load: true
tags: oncall/engine:latest
cache-from: type=gha
cache-to: type=gha,mode=max
outputs: type=docker,dest=/tmp/oncall-engine.tar
- name: Load engine Docker image on the nodes of the cluster
run: kind load image-archive --name=chart-testing /tmp/oncall-engine.tar
# spin up 2 engine, 2 celery, and 2 grafana pods, this will allow us to parralelize the integration tests
# and complete them much faster by using multiple test processes
# With just 1 engine/celery/grafana pod, the backend crawls to a halt when there is > 1 parallelized integration
# test process
#
# by settings grafana.plugins to [] and configuring grafana.extraVolumeMounts we are using the locally built
# OnCall plugin rather than the latest published version
# the /oncall-plugin hostPath refers to the kind volumeMount that points to the ./grafana-plugin dir
# see ./helm/kind.yml for more details
- name: Install helm chart
run: |
helm install helm-testing \
--values ./helm/simple.yml \
--values ./helm/values-local-image.yml \
--set-json 'env=[{"name":"GRAFANA_CLOUD_NOTIFICATIONS_ENABLED","value":"False"}]' \
--set engine.replicaCount=1 \
--set celery.replicaCount=1 \
--set celery.worker_beat_enabled="False" \
--set oncall.twilio.accountSid="${{ secrets.TWILIO_ACCOUNT_SID }}" \
--set oncall.twilio.authToken="${{ secrets.TWILIO_AUTH_TOKEN }}" \
--set oncall.twilio.phoneNumber="\"${{ secrets.TWILIO_PHONE_NUMBER }}"\" \
--set oncall.twilio.verifySid="${{ secrets.TWILIO_VERIFY_SID }}" \
--set grafana.replicas=1 \
--set grafana.image.tag=${{ matrix.grafana-image-tag }} \
--set grafana.env.GF_SECURITY_ADMIN_USER=oncall \
--set grafana.env.GF_SECURITY_ADMIN_PASSWORD=oncall \
--set grafana.env.GF_FEATURE_TOGGLES_ENABLE=topnav \
--set grafana.env.GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=grafana-oncall-app \
--set-json "grafana.plugins=[]" \
--set-json 'grafana.securityContext={"runAsUser": 0, "runAsGroup": 0, "fsGroup": 0}' \
--set-json 'grafana.extraVolumeMounts=[{"name":"plugins","mountPath":"/var/lib/grafana/plugins/grafana-plugin","hostPath":"/oncall-plugin","readOnly":true}]' \
./helm/oncall
# helpful reference for properly caching the playwright binaries/dependencies
# https://playwrightsolutions.com/playwright-github-action-to-cache-the-browser-binaries/
- name: Get installed Playwright version
id: playwright-version
working-directory: grafana-plugin
run: echo "PLAYWRIGHT_VERSION=$(cat ./package.json | jq -r '.devDependencies["@playwright/test"]')" >> $GITHUB_ENV
- name: Cache Playwright binaries/dependencies
id: playwright-cache
uses: actions/cache@v3
with:
path: "~/.cache/ms-playwright"
key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }}
- name: Install Playwright binaries/dependencies
if: steps.playwright-cache.outputs.cache-hit != 'true'
# if more browsers are added, will need to modify the "npx playwright install" command
# https://stackoverflow.com/questions/65900299/install-single-dependency-from-package-json-with-yarn
run: |
yarn add "@playwright/test@${{ env.PLAYWRIGHT_VERSION }}"
npx playwright install --with-deps chromium firefox
npx playwright install-deps
- name: Await k8s pods and other resources up
uses: jupyterhub/action-k8s-await-workloads@v1
with:
workloads: "" # all
namespace: "" # default
timeout: 300
max-restarts: -1
- name: Run Integration Tests
env:
# BASE_URL represents what is accessed via a browser
BASE_URL: http://localhost:30002/grafana
# ONCALL_API_URL is what is configured in the plugin configuration form
# it is what the grafana container uses to communicate with the OnCall backend
#
# 172.17.0.1 is the docker bridge network default gateway. Requests originate in the grafana container
# hit 172.17.0.1 which proxies the request onto the host where port 30001 is the node port that is mapped
# to the OnCall API
ONCALL_API_URL: http://172.17.0.1:30001
GRAFANA_USERNAME: oncall
GRAFANA_PASSWORD: oncall
MAILSLURP_API_KEY: ${{ secrets.MAILSLURP_API_KEY }}
working-directory: ./grafana-plugin
# -x = exit command after first failing test
run: yarn test:integration -x
# always spit out the engine and celery logs, AFTER the e2e tests have completed
# can be helpful for debugging failing/flaky tests
# GitHub Action reference: https://github.com/jupyterhub/action-k8s-namespace-report
- name: Kubernetes namespace report
uses: jupyterhub/action-k8s-namespace-report@v1
if: failure()
with:
important-workloads: "deploy/helm-testing-oncall-engine deploy/helm-testing-oncall-celery"
- uses: actions/upload-artifact@v3
if: failure()
with:
name: playwright-report-grafana-${{ matrix.grafana-image-tag }}
path: ./grafana-plugin/playwright-report/
retention-days: 30

View file

@ -1,4 +1,4 @@
name: "publish-technical-documentation-next"
name: "Publish Technical Documentation (next)"
on:
push:

View file

@ -1,4 +1,4 @@
name: "publish-technical-documentation-release"
name: "Publish Technical Documentation (release)"
on:
push:

View file

@ -17,16 +17,21 @@ jobs:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.9"
python-version: "3.9.12"
cache: "pip"
cache-dependency-path: engine/requirements.txt
- uses: actions/setup-node@v3
with:
node-version: 14.17.0
cache: "yarn"
cache-dependency-path: grafana-plugin/yarn.lock
- uses: snyk/actions/setup@master
- name: Install Dependencies
run: |
pip install -r engine/requirements.txt
cd grafana-plugin/
yarn --network-timeout 500000
- name: Install backend dependencies
working-directory: engine
run: pip install -r requirements.txt
- name: Install frontend dependencies
working-directory: grafana-plugin
run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000
- name: Run Snyk
continue-on-error: true
run: snyk test --all-projects --severity-threshold=high

View file

@ -21,7 +21,7 @@ else:
}
if BROKER_TYPE == BrokerTypes.RABBITMQ:
CELERY_BROKER_URL = "amqp://rabbitmq:rabbitmq@rabbit_test:5672"
CELERY_BROKER_URL = RABBITMQ_URI
elif BROKER_TYPE == BrokerTypes.REDIS:
CELERY_BROKER_URL = REDIS_URI

View file

@ -1,7 +1,7 @@
#!/usr/bin/env bash
until nc -z -v -w30 mysql_test 3306
until nc -z -v -w30 "${DATABASE_HOST:mysql_test}" 3306
do
echo "Waiting for database connection..."
sleep 1
done
done

View file

@ -1,15 +1,10 @@
import { test } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { verifyThatAlertGroupIsTriggered } from '../utils/alertGroup';
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
import { generateRandomValue } from '../utils/forms';
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
import { createOnCallSchedule } from '../utils/schedule';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
test('we can create an oncall schedule + receive an alert', async ({ page }) => {
const escalationChainName = generateRandomValue();
const integrationName = generateRandomValue();

View file

@ -1,5 +1,4 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { GRAFANA_USERNAME } from '../utils/constants';
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
import { generateRandomValue } from '../utils/forms';
@ -7,10 +6,6 @@ import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
import { waitForSms } from '../utils/phone';
import { configureUserNotificationSettings, verifyUserPhoneNumber } from '../utils/userSettings';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
// TODO: enable once we've signed up for a MailSlurp account to receieve SMSes
test.skip('we can verify our phone number + receive an SMS alert', async ({ page }) => {
const escalationChainName = generateRandomValue();

View file

@ -1,12 +1,7 @@
import { test, expect, Page } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { generateRandomValue } from '../utils/forms';
import { createEscalationChain } from '../utils/escalationChain';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
const assertEscalationChainSearchWorks = async (
page: Page,
searchTerm: string,
@ -20,18 +15,18 @@ const assertEscalationChainSearchWorks = async (
await expect(page.getByTestId('escalation-chains-list')).toHaveText(escalationChainFullName);
};
test('searching allows case-insensitive partial matches', async ({ page }) => {
// TODO: add tests for the new filtering. Commented out as this search doesn't exist anymore
test.skip('searching allows case-insensitive partial matches', async ({ page }) => {
const escalationChainName = `${generateRandomValue()} ${generateRandomValue()}`;
// const [firstHalf, secondHalf] = escalationChainName.split(' ');
const [firstHalf, secondHalf] = escalationChainName.split(' ');
await createEscalationChain(page, escalationChainName);
// Commented as this search doesn't exist anymore TODO: add tests for the new filtering
// await assertEscalationChainSearchWorks(page, firstHalf, escalationChainName);
// await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName);
// await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName);
//
// await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName);
// await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName);
// await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, firstHalf, escalationChainName);
await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName);
await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName);
await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName);
});

View file

@ -1,6 +1,39 @@
import { chromium, FullConfig, expect } from '@playwright/test';
import { chromium, FullConfig, expect, Page } from '@playwright/test';
import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME } from './utils/constants';
import { BASE_URL, GRAFANA_PASSWORD, GRAFANA_USERNAME, IS_OPEN_SOURCE, ONCALL_API_URL } from './utils/constants';
import { clickButton, getInputByName } from './utils/forms';
import { goToGrafanaPage } from './utils/navigation';
/**
* go to config page and wait for plugin icon to be available on left-hand navigation
*/
export const configureOnCallPlugin = async (page: Page): Promise<void> => {
// plugin configuration can safely be skipped for non open-source environments
if (!IS_OPEN_SOURCE) {
return;
}
/**
* go to the oncall plugin configuration page and wait for the page to be loaded
*/
await goToGrafanaPage(page, '/plugins/grafana-oncall-app');
await page.waitForSelector('text=Configure Grafana OnCall');
/**
* we may need to fill in the OnCall API URL if it is not set in the process.env
* of the frontend build
*/
const onCallApiUrlInput = getInputByName(page, 'onCallApiUrl');
const pluginIsAutoConfigured = (await onCallApiUrlInput.count()) === 0;
if (!pluginIsAutoConfigured) {
await onCallApiUrlInput.fill(ONCALL_API_URL);
await clickButton({ page, buttonText: 'Connect' });
}
// wait for the "Connected to OnCall" message to know that everything is properly configured
await expect(page.getByTestId('status-message-block')).toHaveText(/Connected to OnCall.*/);
};
/**
* Borrowed from our friends on the Incident team
@ -20,6 +53,11 @@ const globalSetup = async (config: FullConfig): Promise<void> => {
expect(res.ok()).toBeTruthy();
await browserContext.storageState({ path: './storageState.json' });
// make sure the plugin has been configured
const page = await browserContext.newPage();
await configureOnCallPlugin(page);
await browserContext.close();
};

View file

@ -1,11 +1,6 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { openCreateIntegrationModal } from '../utils/integrations';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
test('integrations have unique names', async ({ page }) => {
await openCreateIntegrationModal(page);

View file

@ -1,13 +1,8 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { clickButton, generateRandomValue } from '../utils/forms';
import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule';
import dayjs from 'dayjs';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
test('default dates in override creation modal are correct', async ({ page }) => {
const onCallScheduleName = generateRandomValue();
await createOnCallSchedule(page, onCallScheduleName);

View file

@ -1,12 +1,7 @@
import { test, expect } from '@playwright/test';
import { configureOnCallPlugin } from '../utils/configurePlugin';
import { generateRandomValue } from '../utils/forms';
import { createOnCallSchedule } from '../utils/schedule';
test.beforeEach(async ({ page }) => {
await configureOnCallPlugin(page);
});
test('check schedule quality for simple 1-user schedule', async ({ page }) => {
const onCallScheduleName = generateRandomValue();
await createOnCallSchedule(page, onCallScheduleName);
@ -14,6 +9,10 @@ test('check schedule quality for simple 1-user schedule', async ({ page }) => {
await expect(page.locator('div[class*="ScheduleQuality"]')).toHaveText('Quality: Great');
await page.hover('div[class*="ScheduleQuality"]');
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=2 ')).toHaveText('Schedule has no gaps');
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText('Schedule is perfectly balanced');
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=2 ')).toHaveText(
'Schedule has no gaps'
);
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText(
'Schedule is perfectly balanced'
);
});

View file

@ -1,38 +0,0 @@
import type { Page } from '@playwright/test';
import { ONCALL_API_URL, IS_OPEN_SOURCE } from './constants';
import { clickButton, getInputByName } from './forms';
import { goToGrafanaPage } from './navigation';
/**
* go to config page and wait for plugin icon to be available on left-hand navigation
*/
export const configureOnCallPlugin = async (page: Page): Promise<void> => {
// plugin configuration can safely be skipped for non open-source environments
if (!IS_OPEN_SOURCE) {
return;
}
/**
* go to the oncall plugin configuration page and wait for the page to be loaded
*/
await goToGrafanaPage(page, '/plugins/grafana-oncall-app');
await page.waitForSelector('text=Configure Grafana OnCall');
/**
* we may need to fill in the OnCall API URL if it is not set in the process.env
* of the frontend build
*/
const onCallApiUrlInput = getInputByName(page, 'onCallApiUrl');
const pluginIsAutoConfigured = (await onCallApiUrlInput.count()) === 0;
if (!pluginIsAutoConfigured) {
await onCallApiUrlInput.fill(ONCALL_API_URL);
await clickButton({ page, buttonText: 'Connect' });
}
/**
* wait for the page to be refreshed and the icon to show up, this means the plugin
* has been successfully configured
*/
await page.waitForSelector('div.scrollbar-view img[src*="grafana-oncall-app/img/logo.svg"]');
};

View file

@ -1,4 +1,4 @@
import { Page } from '@playwright/test';
import { expect, Page } from '@playwright/test';
import { clickButton, fillInInput, selectDropdownValue } from './forms';
import { goToOnCallPage } from './navigation';
@ -22,6 +22,14 @@ export const createEscalationChain = async (
// go to the escalation chains page
await goToOnCallPage(page, 'escalations');
/**
* wait for Esclation Chains page to fully load. this is because this can change which "New Escalation Chain"
* button is present
* ie. the one on the left hand side in the list vs the one in the center when no escalation chains exist
*/
await page.getByTestId('page-title').locator('text=Escalation Chains').waitFor({ state: 'visible' });
await page.locator('text=Loading...').waitFor({ state: 'detached' });
// open the create escalation chain modal
(await page.waitForSelector('text=New Escalation Chain')).click();
@ -30,7 +38,7 @@ export const createEscalationChain = async (
// submit the form and wait for it to be created
await clickButton({ page, buttonText: 'Create' });
await page.waitForSelector(`text=${escalationChainName}`);
await expect(page.getByTestId('escalation-chain-name')).toHaveText(escalationChainName);
if (!escalationStep || !escalationStepValue) {
return;

View file

@ -54,7 +54,7 @@ const openSelect = async ({
placeholderText,
selectType,
startingLocator,
}: SelectDropdownValueArgs): Promise<void> => {
}: SelectDropdownValueArgs): Promise<Locator> => {
/**
* we currently mix three different dropdown components in the UI..
* so we need to support all of them :(
@ -73,6 +73,8 @@ const openSelect = async ({
const selectElement: Locator = (startingLocator || page).locator(selector);
await selectElement.waitFor({ state: 'visible' });
await selectElement.click();
return selectElement;
};
/**
@ -85,7 +87,15 @@ const chooseDropdownValue = async ({ page, value, optionExactMatch = true }: Sel
page.locator(`div[id^="react-select-"][id$="-listbox"] >> ${textMatchSelector(optionExactMatch, value)}`).click();
export const selectDropdownValue = async (args: SelectDropdownValueArgs): Promise<void> => {
await openSelect(args);
const selectElement = await openSelect(args);
/**
* use the select search to filter down the options
* TODO: get rid of the slice when we fix the GSelect component..
* without slicing this would fire off an API request for every key-stroke
*/
await selectElement.type(args.value.slice(0, 5));
await chooseDropdownValue(args);
};

View file

@ -59,7 +59,7 @@
"@grafana/eslint-config": "^5.0.0",
"@grafana/toolkit": "^9.2.4",
"@jest/globals": "^27.5.1",
"@playwright/test": "^1.28.0",
"@playwright/test": "^1.32.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "12",
"@testing-library/user-event": "^14.4.3",

View file

@ -14,7 +14,8 @@ const config: PlaywrightTestConfig = {
testDir: './integration-tests',
globalSetup: './integration-tests/globalSetup.ts',
/* Maximum time one test can run for. */
timeout: 60 * 1000,
// TODO: set this back to 60 when GSelect component is refactored
timeout: 90 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
@ -27,8 +28,10 @@ const config: PlaywrightTestConfig = {
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
retries: process.env.CI ? 1 : 0,
// TODO: when GSelect component is refactored, run using 3 workers
// locally use one worker, on CI use 3
// workers: process.env.CI ? 3 : 1,
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',

View file

@ -23,7 +23,11 @@ function RealPlugin(props: AppPluginPageProps): React.ReactNode {
{/* Render alerts at the top */}
<Alerts />
<Header backendLicense={store.backendLicense} />
{pages[page]?.text && !pages[page]?.hideTitle && <h3 className="page-title">{pages[page].text}</h3>}
{pages[page]?.text && !pages[page]?.hideTitle && (
<h3 className="page-title" data-testid="page-title">
{pages[page].text}
</h3>
)}
{props.children}
</RealPluginPage>
);

View file

@ -297,7 +297,6 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
className={cx('select', 'control')}
value={notify_schedule}
onChange={this._getOnChangeHandler('notify_schedule')}
fromOrganization
getOptionLabel={(item: SelectableValue) => {
const team = teamStore.items[scheduleStore.items[item.value].team];
return (
@ -351,7 +350,6 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
className={cx('select', 'control')}
value={custom_button_trigger}
onChange={this._getOnChangeHandler('custom_button_trigger')}
fromOrganization
getOptionLabel={(item: SelectableValue) => {
const team = teamStore.items[outgoingWebhookStore.items[item.value].team];
return (

View file

@ -9,6 +9,7 @@ import { observer } from 'mobx-react';
import { useStore } from 'state/useStore';
import styles from './GSelect.module.css';
// import { debounce } from 'lodash';
const cx = cn.bind(styles);
@ -30,7 +31,6 @@ interface GSelectProps {
showWarningIfEmptyValue?: boolean;
showError?: boolean;
nullItemName?: string;
fromOrganization?: boolean;
filterOptions?: (id: any) => boolean;
dropdownRender?: (menu: ReactElement) => ReactElement;
getOptionLabel?: <T>(item: SelectableValue<T>) => React.ReactNode;
@ -61,7 +61,6 @@ const GSelect = observer((props: GSelectProps) => {
showWarningIfEmptyValue = false,
getDescription,
filterOptions,
// fromOrganization,
width = null,
icon = null,
} = props;
@ -89,6 +88,11 @@ const GSelect = observer((props: GSelectProps) => {
[model, onChange]
);
/**
* without debouncing this function when search is available
* we risk hammering the API endpoint for every single key stroke
* some context on 250ms as the choice here - https://stackoverflow.com/a/44755058/3902555
*/
const loadOptions = (query: string) => {
return model.updateItems(query).then(() => {
const searchResult = model.getSearchResult(query);
@ -106,6 +110,9 @@ const GSelect = observer((props: GSelectProps) => {
});
};
// TODO: why doesn't this work properly?
// const loadOptions = debounce(_loadOptions, showSearch ? 250 : 0);
const values = isMulti
? (value ? (value as string[]) : [])
.filter((id) => id in model.items)

View file

@ -287,7 +287,12 @@ class EscalationChainsPage extends React.Component<EscalationChainsPageProps, Es
return (
<>
<Block withBackground className={cx('header')}>
<Text size="large" editable onTextChange={this.handleEscalationChainNameChange}>
<Text
size="large"
editable
onTextChange={this.handleEscalationChainNameChange}
data-testid="escalation-chain-name"
>
{escalationChain.name}
</Text>
<div className={cx('buttons')}>

View file

@ -2603,13 +2603,15 @@
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.6.6.tgz#641f73913a6be402b34e4bdfca98d6832ed55586"
integrity sha512-3MUulwMtsdCA9lw8a/Kc0XDBJJVCkYTQ5aGd+///TbfkOMXoOGAzzoiYKwPEsLYZv7He7fKJ/mCacqKOO7REyg==
"@playwright/test@^1.28.0":
version "1.28.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.28.0.tgz#8de83f9d2291bba3f37883e33431b325661720d9"
integrity sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==
"@playwright/test@^1.32.0":
version "1.32.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.32.0.tgz#0cc4c179e62995cc123adb12fdfaa093fed282c4"
integrity sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==
dependencies:
"@types/node" "*"
playwright-core "1.28.0"
playwright-core "1.32.0"
optionalDependencies:
fsevents "2.3.2"
"@polka/url@^1.0.0-next.20":
version "1.0.0-next.21"
@ -7083,7 +7085,7 @@ fs.realpath@^1.0.0:
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@^2.3.2, fsevents@~2.3.2:
fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@ -10347,10 +10349,10 @@ pkg-up@^3.1.0:
dependencies:
find-up "^3.0.0"
playwright-core@1.28.0:
version "1.28.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.28.0.tgz#61df5c714f45139cca07095eccb4891e520e06f2"
integrity sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==
playwright-core@1.32.0:
version "1.32.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.32.0.tgz#730c2d1988d30377480b925aaa6c1b1e2442d67e"
integrity sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==
please-upgrade-node@^3.2.0:
version "3.2.0"