From 23cd736c30f203c0d6977b0ee75bf5bda3eb2304 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Mon, 27 Mar 2023 18:07:19 +0200 Subject: [PATCH] speed up ci builds from 15 to <7 minutes (#1615) This PR cuts GitHub Action build times from 14-15 minutes, down to just under 7 minutes. It does this by: - caching `grafana-plugins/node_modules` and `pip` dependencies based on their respective dependency files (eg. `requirements.txt` & `yarn.lock`). This step alone saves ~3 minutes. - get rid of the "build-engine-docker-image" and "backend-integration-tests" jobs in the old "Integration Tests" workflow. This was split out this way so that we could build the backend docker image once, upload the artifact, and then reuse it across the backend and e2e tests. We no longer need these backend integration tests because we are testing the same thing in the e2e tests. This saves ~45 seconds of having to upload the image artifact. - few improvements within the integration tests themselves: - move plugin configuration to the `globalSetup.ts`. This means that every test does not need to check if the plugin has been configured because it is done once before all the tests are run. - cache the plugin frontend build. If your commit doesn't change anything to `grafana-plugin/src` or `grafana-plugin/yarn.lock` it should be safe to reuse a previously built/cached version of the plugin frontend. This saves ~3 minutes - cache playwright binaries/dependencies. Only re-install them if the version of `@playwright/test` in `grafana-plugin/yarn.lock` changes. This saves ~3 minutes. **Other things to mention** Once we refactor the `GSelect` component to not call the `onChange` callback on every keyDown event (#1628), this should allow us to parallelize the integration tests, and cut the time required to execute the tests themselves in half --- .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..25017e27 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"