Revert "Revert "speed up ci builds from 15 to <7 minutes"" (#1643)
Reverts grafana/oncall#1639
This commit is contained in:
parent
61608beee6
commit
0eb4bd95e6
27 changed files with 562 additions and 624 deletions
211
.github/workflows/ci.yml
vendored
211
.github/workflows/ci.yml
vendored
|
|
@ -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 }}
|
|
||||||
2
.github/workflows/helm_release.yml
vendored
2
.github/workflows/helm_release.yml
vendored
|
|
@ -1,4 +1,4 @@
|
||||||
name: helm-release
|
name: Helm Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
||||||
294
.github/workflows/integration_tests.yml
vendored
294
.github/workflows/integration_tests.yml
vendored
|
|
@ -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
|
|
||||||
2
.github/workflows/issue_commands.yml
vendored
2
.github/workflows/issue_commands.yml
vendored
|
|
@ -9,7 +9,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Actions
|
- name: Checkout Actions
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
repository: "grafana/grafana-github-actions"
|
repository: "grafana/grafana-github-actions"
|
||||||
path: ./actions
|
path: ./actions
|
||||||
|
|
|
||||||
427
.github/workflows/linting-and-tests.yml
vendored
Normal file
427
.github/workflows/linting-and-tests.yml
vendored
Normal 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
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
name: "publish-technical-documentation-next"
|
name: "Publish Technical Documentation (next)"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
name: "publish-technical-documentation-release"
|
name: "Publish Technical Documentation (release)"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
|
|
||||||
17
.github/workflows/snyk.yml
vendored
17
.github/workflows/snyk.yml
vendored
|
|
@ -17,16 +17,21 @@ jobs:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: "3.9"
|
python-version: "3.9.12"
|
||||||
|
cache: "pip"
|
||||||
|
cache-dependency-path: engine/requirements.txt
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 14.17.0
|
node-version: 14.17.0
|
||||||
|
cache: "yarn"
|
||||||
|
cache-dependency-path: grafana-plugin/yarn.lock
|
||||||
- uses: snyk/actions/setup@master
|
- uses: snyk/actions/setup@master
|
||||||
- name: Install Dependencies
|
- name: Install backend dependencies
|
||||||
run: |
|
working-directory: engine
|
||||||
pip install -r engine/requirements.txt
|
run: pip install -r requirements.txt
|
||||||
cd grafana-plugin/
|
- name: Install frontend dependencies
|
||||||
yarn --network-timeout 500000
|
working-directory: grafana-plugin
|
||||||
|
run: yarn install --frozen-lockfile --prefer-offline --network-timeout 500000
|
||||||
- name: Run Snyk
|
- name: Run Snyk
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: snyk test --all-projects --severity-threshold=high
|
run: snyk test --all-projects --severity-threshold=high
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ else:
|
||||||
}
|
}
|
||||||
|
|
||||||
if BROKER_TYPE == BrokerTypes.RABBITMQ:
|
if BROKER_TYPE == BrokerTypes.RABBITMQ:
|
||||||
CELERY_BROKER_URL = "amqp://rabbitmq:rabbitmq@rabbit_test:5672"
|
CELERY_BROKER_URL = RABBITMQ_URI
|
||||||
elif BROKER_TYPE == BrokerTypes.REDIS:
|
elif BROKER_TYPE == BrokerTypes.REDIS:
|
||||||
CELERY_BROKER_URL = REDIS_URI
|
CELERY_BROKER_URL = REDIS_URI
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
until nc -z -v -w30 mysql_test 3306
|
until nc -z -v -w30 "${DATABASE_HOST:=mysql_test}" 3306
|
||||||
do
|
do
|
||||||
echo "Waiting for database connection..."
|
echo "Waiting for database connection..."
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
import { test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
import { configureOnCallPlugin } from '../utils/configurePlugin';
|
|
||||||
import { verifyThatAlertGroupIsTriggered } from '../utils/alertGroup';
|
import { verifyThatAlertGroupIsTriggered } from '../utils/alertGroup';
|
||||||
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
||||||
import { generateRandomValue } from '../utils/forms';
|
import { generateRandomValue } from '../utils/forms';
|
||||||
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
|
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
|
||||||
import { createOnCallSchedule } from '../utils/schedule';
|
import { createOnCallSchedule } from '../utils/schedule';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await configureOnCallPlugin(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('we can create an oncall schedule + receive an alert', async ({ page }) => {
|
test('we can create an oncall schedule + receive an alert', async ({ page }) => {
|
||||||
const escalationChainName = generateRandomValue();
|
const escalationChainName = generateRandomValue();
|
||||||
const integrationName = generateRandomValue();
|
const integrationName = generateRandomValue();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { configureOnCallPlugin } from '../utils/configurePlugin';
|
|
||||||
import { GRAFANA_USERNAME } from '../utils/constants';
|
import { GRAFANA_USERNAME } from '../utils/constants';
|
||||||
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
||||||
import { generateRandomValue } from '../utils/forms';
|
import { generateRandomValue } from '../utils/forms';
|
||||||
|
|
@ -7,10 +6,6 @@ import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
|
||||||
import { waitForSms } from '../utils/phone';
|
import { waitForSms } from '../utils/phone';
|
||||||
import { configureUserNotificationSettings, verifyUserPhoneNumber } from '../utils/userSettings';
|
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
|
// 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 }) => {
|
test.skip('we can verify our phone number + receive an SMS alert', async ({ page }) => {
|
||||||
const escalationChainName = generateRandomValue();
|
const escalationChainName = generateRandomValue();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
import { test, expect, Page } from '@playwright/test';
|
import { test, expect, Page } from '@playwright/test';
|
||||||
import { configureOnCallPlugin } from '../utils/configurePlugin';
|
|
||||||
import { generateRandomValue } from '../utils/forms';
|
import { generateRandomValue } from '../utils/forms';
|
||||||
import { createEscalationChain } from '../utils/escalationChain';
|
import { createEscalationChain } from '../utils/escalationChain';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await configureOnCallPlugin(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
const assertEscalationChainSearchWorks = async (
|
const assertEscalationChainSearchWorks = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
|
|
@ -20,18 +15,18 @@ const assertEscalationChainSearchWorks = async (
|
||||||
await expect(page.getByTestId('escalation-chains-list')).toHaveText(escalationChainFullName);
|
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 escalationChainName = `${generateRandomValue()} ${generateRandomValue()}`;
|
||||||
// const [firstHalf, secondHalf] = escalationChainName.split(' ');
|
const [firstHalf, secondHalf] = escalationChainName.split(' ');
|
||||||
|
|
||||||
await createEscalationChain(page, escalationChainName);
|
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, escalationChainName);
|
await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName);
|
||||||
// await assertEscalationChainSearchWorks(page, firstHalf.toUpperCase(), escalationChainName);
|
await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName);
|
||||||
// await assertEscalationChainSearchWorks(page, firstHalf.toLowerCase(), escalationChainName);
|
|
||||||
//
|
await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName);
|
||||||
// await assertEscalationChainSearchWorks(page, secondHalf, escalationChainName);
|
await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName);
|
||||||
// await assertEscalationChainSearchWorks(page, secondHalf.toUpperCase(), escalationChainName);
|
await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName);
|
||||||
// await assertEscalationChainSearchWorks(page, secondHalf.toLowerCase(), escalationChainName);
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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
|
* Borrowed from our friends on the Incident team
|
||||||
|
|
@ -20,6 +53,11 @@ const globalSetup = async (config: FullConfig): Promise<void> => {
|
||||||
|
|
||||||
expect(res.ok()).toBeTruthy();
|
expect(res.ok()).toBeTruthy();
|
||||||
await browserContext.storageState({ path: './storageState.json' });
|
await browserContext.storageState({ path: './storageState.json' });
|
||||||
|
|
||||||
|
// make sure the plugin has been configured
|
||||||
|
const page = await browserContext.newPage();
|
||||||
|
await configureOnCallPlugin(page);
|
||||||
|
|
||||||
await browserContext.close();
|
await browserContext.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,6 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { configureOnCallPlugin } from '../utils/configurePlugin';
|
|
||||||
import { openCreateIntegrationModal } from '../utils/integrations';
|
import { openCreateIntegrationModal } from '../utils/integrations';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await configureOnCallPlugin(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('integrations have unique names', async ({ page }) => {
|
test('integrations have unique names', async ({ page }) => {
|
||||||
await openCreateIntegrationModal(page);
|
await openCreateIntegrationModal(page);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { configureOnCallPlugin } from '../utils/configurePlugin';
|
|
||||||
import { clickButton, generateRandomValue } from '../utils/forms';
|
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||||
import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule';
|
import { createOnCallSchedule, getOverrideFormDateInputs } from '../utils/schedule';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await configureOnCallPlugin(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('default dates in override creation modal are correct', async ({ page }) => {
|
test('default dates in override creation modal are correct', async ({ page }) => {
|
||||||
const onCallScheduleName = generateRandomValue();
|
const onCallScheduleName = generateRandomValue();
|
||||||
await createOnCallSchedule(page, onCallScheduleName);
|
await createOnCallSchedule(page, onCallScheduleName);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { configureOnCallPlugin } from '../utils/configurePlugin';
|
|
||||||
import { generateRandomValue } from '../utils/forms';
|
import { generateRandomValue } from '../utils/forms';
|
||||||
import { createOnCallSchedule } from '../utils/schedule';
|
import { createOnCallSchedule } from '../utils/schedule';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await configureOnCallPlugin(page);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('check schedule quality for simple 1-user schedule', async ({ page }) => {
|
test('check schedule quality for simple 1-user schedule', async ({ page }) => {
|
||||||
const onCallScheduleName = generateRandomValue();
|
const onCallScheduleName = generateRandomValue();
|
||||||
await createOnCallSchedule(page, onCallScheduleName);
|
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 expect(page.locator('div[class*="ScheduleQuality"]')).toHaveText('Quality: Great');
|
||||||
|
|
||||||
await page.hover('div[class*="ScheduleQuality"]');
|
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=2 ')).toHaveText(
|
||||||
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText('Schedule is perfectly balanced');
|
'Schedule has no gaps'
|
||||||
|
);
|
||||||
|
await expect(page.locator('div[class*="ScheduleQualityDetails"] >> span[class*="Text"] >> nth=3 ')).toHaveText(
|
||||||
|
'Schedule is perfectly balanced'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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"]');
|
|
||||||
};
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Page } from '@playwright/test';
|
import { expect, Page } from '@playwright/test';
|
||||||
|
|
||||||
import { clickButton, fillInInput, selectDropdownValue } from './forms';
|
import { clickButton, fillInInput, selectDropdownValue } from './forms';
|
||||||
import { goToOnCallPage } from './navigation';
|
import { goToOnCallPage } from './navigation';
|
||||||
|
|
@ -22,6 +22,14 @@ export const createEscalationChain = async (
|
||||||
// go to the escalation chains page
|
// go to the escalation chains page
|
||||||
await goToOnCallPage(page, 'escalations');
|
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
|
// open the create escalation chain modal
|
||||||
(await page.waitForSelector('text=New Escalation Chain')).click();
|
(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
|
// submit the form and wait for it to be created
|
||||||
await clickButton({ page, buttonText: 'Create' });
|
await clickButton({ page, buttonText: 'Create' });
|
||||||
await page.waitForSelector(`text=${escalationChainName}`);
|
await expect(page.getByTestId('escalation-chain-name')).toHaveText(escalationChainName);
|
||||||
|
|
||||||
if (!escalationStep || !escalationStepValue) {
|
if (!escalationStep || !escalationStepValue) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ const openSelect = async ({
|
||||||
placeholderText,
|
placeholderText,
|
||||||
selectType,
|
selectType,
|
||||||
startingLocator,
|
startingLocator,
|
||||||
}: SelectDropdownValueArgs): Promise<void> => {
|
}: SelectDropdownValueArgs): Promise<Locator> => {
|
||||||
/**
|
/**
|
||||||
* we currently mix three different dropdown components in the UI..
|
* we currently mix three different dropdown components in the UI..
|
||||||
* so we need to support all of them :(
|
* so we need to support all of them :(
|
||||||
|
|
@ -73,6 +73,8 @@ const openSelect = async ({
|
||||||
const selectElement: Locator = (startingLocator || page).locator(selector);
|
const selectElement: Locator = (startingLocator || page).locator(selector);
|
||||||
await selectElement.waitFor({ state: 'visible' });
|
await selectElement.waitFor({ state: 'visible' });
|
||||||
await selectElement.click();
|
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();
|
page.locator(`div[id^="react-select-"][id$="-listbox"] >> ${textMatchSelector(optionExactMatch, value)}`).click();
|
||||||
|
|
||||||
export const selectDropdownValue = async (args: SelectDropdownValueArgs): Promise<void> => {
|
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);
|
await chooseDropdownValue(args);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
"@grafana/eslint-config": "^5.0.0",
|
"@grafana/eslint-config": "^5.0.0",
|
||||||
"@grafana/toolkit": "^9.2.4",
|
"@grafana/toolkit": "^9.2.4",
|
||||||
"@jest/globals": "^27.5.1",
|
"@jest/globals": "^27.5.1",
|
||||||
"@playwright/test": "^1.28.0",
|
"@playwright/test": "^1.32.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/react": "12",
|
"@testing-library/react": "12",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ const config: PlaywrightTestConfig = {
|
||||||
testDir: './integration-tests',
|
testDir: './integration-tests',
|
||||||
globalSetup: './integration-tests/globalSetup.ts',
|
globalSetup: './integration-tests/globalSetup.ts',
|
||||||
/* Maximum time one test can run for. */
|
/* 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: {
|
expect: {
|
||||||
/**
|
/**
|
||||||
* Maximum time expect() should wait for the condition to be met.
|
* 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. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 1 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
// 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,
|
workers: 1,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,11 @@ function RealPlugin(props: AppPluginPageProps): React.ReactNode {
|
||||||
{/* Render alerts at the top */}
|
{/* Render alerts at the top */}
|
||||||
<Alerts />
|
<Alerts />
|
||||||
<Header backendLicense={store.backendLicense} />
|
<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}
|
{props.children}
|
||||||
</RealPluginPage>
|
</RealPluginPage>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,6 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
||||||
className={cx('select', 'control')}
|
className={cx('select', 'control')}
|
||||||
value={notify_schedule}
|
value={notify_schedule}
|
||||||
onChange={this._getOnChangeHandler('notify_schedule')}
|
onChange={this._getOnChangeHandler('notify_schedule')}
|
||||||
fromOrganization
|
|
||||||
getOptionLabel={(item: SelectableValue) => {
|
getOptionLabel={(item: SelectableValue) => {
|
||||||
const team = teamStore.items[scheduleStore.items[item.value].team];
|
const team = teamStore.items[scheduleStore.items[item.value].team];
|
||||||
return (
|
return (
|
||||||
|
|
@ -351,7 +350,6 @@ export class EscalationPolicy extends React.Component<EscalationPolicyProps, any
|
||||||
className={cx('select', 'control')}
|
className={cx('select', 'control')}
|
||||||
value={custom_button_trigger}
|
value={custom_button_trigger}
|
||||||
onChange={this._getOnChangeHandler('custom_button_trigger')}
|
onChange={this._getOnChangeHandler('custom_button_trigger')}
|
||||||
fromOrganization
|
|
||||||
getOptionLabel={(item: SelectableValue) => {
|
getOptionLabel={(item: SelectableValue) => {
|
||||||
const team = teamStore.items[outgoingWebhookStore.items[item.value].team];
|
const team = teamStore.items[outgoingWebhookStore.items[item.value].team];
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { observer } from 'mobx-react';
|
||||||
import { useStore } from 'state/useStore';
|
import { useStore } from 'state/useStore';
|
||||||
|
|
||||||
import styles from './GSelect.module.css';
|
import styles from './GSelect.module.css';
|
||||||
|
// import { debounce } from 'lodash';
|
||||||
|
|
||||||
const cx = cn.bind(styles);
|
const cx = cn.bind(styles);
|
||||||
|
|
||||||
|
|
@ -30,7 +31,6 @@ interface GSelectProps {
|
||||||
showWarningIfEmptyValue?: boolean;
|
showWarningIfEmptyValue?: boolean;
|
||||||
showError?: boolean;
|
showError?: boolean;
|
||||||
nullItemName?: string;
|
nullItemName?: string;
|
||||||
fromOrganization?: boolean;
|
|
||||||
filterOptions?: (id: any) => boolean;
|
filterOptions?: (id: any) => boolean;
|
||||||
dropdownRender?: (menu: ReactElement) => ReactElement;
|
dropdownRender?: (menu: ReactElement) => ReactElement;
|
||||||
getOptionLabel?: <T>(item: SelectableValue<T>) => React.ReactNode;
|
getOptionLabel?: <T>(item: SelectableValue<T>) => React.ReactNode;
|
||||||
|
|
@ -61,7 +61,6 @@ const GSelect = observer((props: GSelectProps) => {
|
||||||
showWarningIfEmptyValue = false,
|
showWarningIfEmptyValue = false,
|
||||||
getDescription,
|
getDescription,
|
||||||
filterOptions,
|
filterOptions,
|
||||||
// fromOrganization,
|
|
||||||
width = null,
|
width = null,
|
||||||
icon = null,
|
icon = null,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
@ -89,6 +88,11 @@ const GSelect = observer((props: GSelectProps) => {
|
||||||
[model, onChange]
|
[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) => {
|
const loadOptions = (query: string) => {
|
||||||
return model.updateItems(query).then(() => {
|
return model.updateItems(query).then(() => {
|
||||||
const searchResult = model.getSearchResult(query);
|
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
|
const values = isMulti
|
||||||
? (value ? (value as string[]) : [])
|
? (value ? (value as string[]) : [])
|
||||||
.filter((id) => id in model.items)
|
.filter((id) => id in model.items)
|
||||||
|
|
|
||||||
|
|
@ -287,7 +287,12 @@ class EscalationChainsPage extends React.Component<EscalationChainsPageProps, Es
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block withBackground className={cx('header')}>
|
<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}
|
{escalationChain.name}
|
||||||
</Text>
|
</Text>
|
||||||
<div className={cx('buttons')}>
|
<div className={cx('buttons')}>
|
||||||
|
|
|
||||||
|
|
@ -2603,13 +2603,15 @@
|
||||||
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.6.6.tgz#641f73913a6be402b34e4bdfca98d6832ed55586"
|
resolved "https://registry.yarnpkg.com/@petamoriken/float16/-/float16-3.6.6.tgz#641f73913a6be402b34e4bdfca98d6832ed55586"
|
||||||
integrity sha512-3MUulwMtsdCA9lw8a/Kc0XDBJJVCkYTQ5aGd+///TbfkOMXoOGAzzoiYKwPEsLYZv7He7fKJ/mCacqKOO7REyg==
|
integrity sha512-3MUulwMtsdCA9lw8a/Kc0XDBJJVCkYTQ5aGd+///TbfkOMXoOGAzzoiYKwPEsLYZv7He7fKJ/mCacqKOO7REyg==
|
||||||
|
|
||||||
"@playwright/test@^1.28.0":
|
"@playwright/test@^1.32.0":
|
||||||
version "1.28.0"
|
version "1.32.0"
|
||||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.28.0.tgz#8de83f9d2291bba3f37883e33431b325661720d9"
|
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.32.0.tgz#0cc4c179e62995cc123adb12fdfaa093fed282c4"
|
||||||
integrity sha512-vrHs5DFTPwYox5SGKq/7TDn/S4q6RA1zArd7uhO6EyP9hj3XgZBBM12ktMbnDQNxh/fL1IUKsTNLxihmsU38lQ==
|
integrity sha512-zOdGloaF0jeec7hqoLqM5S3L2rR4WxMJs6lgiAeR70JlH7Ml54ZPoIIf3X7cvnKde3Q9jJ/gaxkFh8fYI9s1rg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
playwright-core "1.28.0"
|
playwright-core "1.32.0"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "2.3.2"
|
||||||
|
|
||||||
"@polka/url@^1.0.0-next.20":
|
"@polka/url@^1.0.0-next.20":
|
||||||
version "1.0.0-next.21"
|
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"
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
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"
|
version "2.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||||
|
|
@ -10347,10 +10349,10 @@ pkg-up@^3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
find-up "^3.0.0"
|
find-up "^3.0.0"
|
||||||
|
|
||||||
playwright-core@1.28.0:
|
playwright-core@1.32.0:
|
||||||
version "1.28.0"
|
version "1.32.0"
|
||||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.28.0.tgz#61df5c714f45139cca07095eccb4891e520e06f2"
|
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.32.0.tgz#730c2d1988d30377480b925aaa6c1b1e2442d67e"
|
||||||
integrity sha512-nJLknd28kPBiCNTbqpu6Wmkrh63OEqJSFw9xOfL9qxfNwody7h6/L3O2dZoWQ6Oxcm0VOHjWmGiCUGkc0X3VZA==
|
integrity sha512-Z9Ij17X5Z3bjpp6XKujGBp9Gv4eViESac9aDmwgQFUEJBW0K80T21m/Z+XJQlu4cNsvPygw33b6V1Va6Bda5zQ==
|
||||||
|
|
||||||
please-upgrade-node@^3.2.0:
|
please-upgrade-node@^3.2.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue