From 0eb4bd95e69c1445c491fc9471798609e583c2a7 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Tue, 28 Mar 2023 09:34:03 +0200 Subject: [PATCH] Revert "Revert "speed up ci builds from 15 to <7 minutes"" (#1643) Reverts grafana/oncall#1639 --- .github/workflows/ci.yml | 211 --------- .github/workflows/helm_release.yml | 2 +- .github/workflows/integration_tests.yml | 294 ------------ .github/workflows/issue_commands.yml | 2 +- .github/workflows/linting-and-tests.yml | 427 ++++++++++++++++++ .../publish-technical-documentation-next.yml | 2 +- ...ublish-technical-documentation-release.yml | 2 +- .github/workflows/snyk.yml | 17 +- engine/settings/ci-test.py | 2 +- engine/wait_for_test_mysql_start.sh | 4 +- .../alerts/onCallSchedule.test.ts | 5 - .../integration-tests/alerts/sms.test.ts | 5 - .../escalationChains/searching.test.ts | 25 +- .../integration-tests/globalSetup.ts | 42 +- .../uniqueIntegrationNames.test.ts | 5 - .../schedules/addOverride.test.ts | 5 - .../schedules/quality.test.ts | 13 +- .../utils/configurePlugin.ts | 38 -- .../utils/escalationChain.ts | 12 +- .../integration-tests/utils/forms.ts | 14 +- grafana-plugin/package.json | 2 +- grafana-plugin/playwright.config.ts | 9 +- grafana-plugin/src/PluginPage.tsx | 6 +- .../components/Policy/EscalationPolicy.tsx | 2 - .../src/containers/GSelect/GSelect.tsx | 11 +- .../escalation-chains/EscalationChains.tsx | 7 +- grafana-plugin/yarn.lock | 22 +- 27 files changed, 562 insertions(+), 624 deletions(-) delete mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/integration_tests.yml create mode 100644 .github/workflows/linting-and-tests.yml delete mode 100644 grafana-plugin/integration-tests/utils/configurePlugin.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 8d3d6d06..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -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 }} diff --git a/.github/workflows/helm_release.yml b/.github/workflows/helm_release.yml index 3c152260..d9fafeab 100644 --- a/.github/workflows/helm_release.yml +++ b/.github/workflows/helm_release.yml @@ -1,4 +1,4 @@ -name: helm-release +name: Helm Release on: push: diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml deleted file mode 100644 index e1d98938..00000000 --- a/.github/workflows/integration_tests.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/issue_commands.yml b/.github/workflows/issue_commands.yml index e33a1468..6187ea56 100644 --- a/.github/workflows/issue_commands.yml +++ b/.github/workflows/issue_commands.yml @@ -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 diff --git a/.github/workflows/linting-and-tests.yml b/.github/workflows/linting-and-tests.yml new file mode 100644 index 00000000..d7b0dcc8 --- /dev/null +++ b/.github/workflows/linting-and-tests.yml @@ -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 diff --git a/.github/workflows/publish-technical-documentation-next.yml b/.github/workflows/publish-technical-documentation-next.yml index f8d1adcb..c242bb78 100644 --- a/.github/workflows/publish-technical-documentation-next.yml +++ b/.github/workflows/publish-technical-documentation-next.yml @@ -1,4 +1,4 @@ -name: "publish-technical-documentation-next" +name: "Publish Technical Documentation (next)" on: push: diff --git a/.github/workflows/publish-technical-documentation-release.yml b/.github/workflows/publish-technical-documentation-release.yml index 82c26cd4..1078d0e0 100644 --- a/.github/workflows/publish-technical-documentation-release.yml +++ b/.github/workflows/publish-technical-documentation-release.yml @@ -1,4 +1,4 @@ -name: "publish-technical-documentation-release" +name: "Publish Technical Documentation (release)" on: push: diff --git a/.github/workflows/snyk.yml b/.github/workflows/snyk.yml index 086cba96..d0035215 100644 --- a/.github/workflows/snyk.yml +++ b/.github/workflows/snyk.yml @@ -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 diff --git a/engine/settings/ci-test.py b/engine/settings/ci-test.py index 23439c13..9751c49d 100644 --- a/engine/settings/ci-test.py +++ b/engine/settings/ci-test.py @@ -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 diff --git a/engine/wait_for_test_mysql_start.sh b/engine/wait_for_test_mysql_start.sh index bb89e036..8eb15a13 100755 --- a/engine/wait_for_test_mysql_start.sh +++ b/engine/wait_for_test_mysql_start.sh @@ -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 \ No newline at end of file +done diff --git a/grafana-plugin/integration-tests/alerts/onCallSchedule.test.ts b/grafana-plugin/integration-tests/alerts/onCallSchedule.test.ts index a8b13efd..800faea6 100644 --- a/grafana-plugin/integration-tests/alerts/onCallSchedule.test.ts +++ b/grafana-plugin/integration-tests/alerts/onCallSchedule.test.ts @@ -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(); diff --git a/grafana-plugin/integration-tests/alerts/sms.test.ts b/grafana-plugin/integration-tests/alerts/sms.test.ts index dac42bb8..f4a1d8f7 100644 --- a/grafana-plugin/integration-tests/alerts/sms.test.ts +++ b/grafana-plugin/integration-tests/alerts/sms.test.ts @@ -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(); diff --git a/grafana-plugin/integration-tests/escalationChains/searching.test.ts b/grafana-plugin/integration-tests/escalationChains/searching.test.ts index 97d0edf8..5013f00f 100644 --- a/grafana-plugin/integration-tests/escalationChains/searching.test.ts +++ b/grafana-plugin/integration-tests/escalationChains/searching.test.ts @@ -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); }); diff --git a/grafana-plugin/integration-tests/globalSetup.ts b/grafana-plugin/integration-tests/globalSetup.ts index 3fc05399..244158d5 100644 --- a/grafana-plugin/integration-tests/globalSetup.ts +++ b/grafana-plugin/integration-tests/globalSetup.ts @@ -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 => { + // 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 => { 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(); }; diff --git a/grafana-plugin/integration-tests/integrations/uniqueIntegrationNames.test.ts b/grafana-plugin/integration-tests/integrations/uniqueIntegrationNames.test.ts index 06fc9284..e3c8df80 100644 --- a/grafana-plugin/integration-tests/integrations/uniqueIntegrationNames.test.ts +++ b/grafana-plugin/integration-tests/integrations/uniqueIntegrationNames.test.ts @@ -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); diff --git a/grafana-plugin/integration-tests/schedules/addOverride.test.ts b/grafana-plugin/integration-tests/schedules/addOverride.test.ts index 3104cc68..f76b6ef9 100644 --- a/grafana-plugin/integration-tests/schedules/addOverride.test.ts +++ b/grafana-plugin/integration-tests/schedules/addOverride.test.ts @@ -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); diff --git a/grafana-plugin/integration-tests/schedules/quality.test.ts b/grafana-plugin/integration-tests/schedules/quality.test.ts index 952e22d8..94b8e2a0 100644 --- a/grafana-plugin/integration-tests/schedules/quality.test.ts +++ b/grafana-plugin/integration-tests/schedules/quality.test.ts @@ -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' + ); }); diff --git a/grafana-plugin/integration-tests/utils/configurePlugin.ts b/grafana-plugin/integration-tests/utils/configurePlugin.ts deleted file mode 100644 index d838cf15..00000000 --- a/grafana-plugin/integration-tests/utils/configurePlugin.ts +++ /dev/null @@ -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 => { - // 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"]'); -}; diff --git a/grafana-plugin/integration-tests/utils/escalationChain.ts b/grafana-plugin/integration-tests/utils/escalationChain.ts index a4fdf3ee..a0f908e4 100644 --- a/grafana-plugin/integration-tests/utils/escalationChain.ts +++ b/grafana-plugin/integration-tests/utils/escalationChain.ts @@ -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; diff --git a/grafana-plugin/integration-tests/utils/forms.ts b/grafana-plugin/integration-tests/utils/forms.ts index 4981ec5f..46142e64 100644 --- a/grafana-plugin/integration-tests/utils/forms.ts +++ b/grafana-plugin/integration-tests/utils/forms.ts @@ -54,7 +54,7 @@ const openSelect = async ({ placeholderText, selectType, startingLocator, -}: SelectDropdownValueArgs): Promise => { +}: SelectDropdownValueArgs): Promise => { /** * 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 => { - 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); }; diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index ae9ad6a4..dfc6334e 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -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", diff --git a/grafana-plugin/playwright.config.ts b/grafana-plugin/playwright.config.ts index 4977535d..e1bfa6d8 100644 --- a/grafana-plugin/playwright.config.ts +++ b/grafana-plugin/playwright.config.ts @@ -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', diff --git a/grafana-plugin/src/PluginPage.tsx b/grafana-plugin/src/PluginPage.tsx index 21a86341..a80120f2 100644 --- a/grafana-plugin/src/PluginPage.tsx +++ b/grafana-plugin/src/PluginPage.tsx @@ -23,7 +23,11 @@ function RealPlugin(props: AppPluginPageProps): React.ReactNode { {/* Render alerts at the top */}
- {pages[page]?.text && !pages[page]?.hideTitle &&

{pages[page].text}

} + {pages[page]?.text && !pages[page]?.hideTitle && ( +

+ {pages[page].text} +

+ )} {props.children} ); diff --git a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx index 329f8a10..ac0811c3 100644 --- a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx @@ -297,7 +297,6 @@ export class EscalationPolicy extends React.Component { const team = teamStore.items[scheduleStore.items[item.value].team]; return ( @@ -351,7 +350,6 @@ export class EscalationPolicy extends React.Component { const team = teamStore.items[outgoingWebhookStore.items[item.value].team]; return ( diff --git a/grafana-plugin/src/containers/GSelect/GSelect.tsx b/grafana-plugin/src/containers/GSelect/GSelect.tsx index a471d2c2..919a9850 100644 --- a/grafana-plugin/src/containers/GSelect/GSelect.tsx +++ b/grafana-plugin/src/containers/GSelect/GSelect.tsx @@ -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?: (item: SelectableValue) => 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) diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index dbcb04ed..9581a193 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -287,7 +287,12 @@ class EscalationChainsPage extends React.Component - + {escalationChain.name}
diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 838cf5ab..57472da4 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -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"