# # This workflow runs CI E2E tests with Cypress. # # It relies on the container image built by 'container-images-ci.yml'. # name: E2E CI on: pull_request: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: changes: runs-on: ubuntu-latest timeout-minutes: 5 name: Determine need to run E2E checks # Set job outputs to values from filter step outputs: shouldTriggerCypress: ${{ steps.changes.outputs.shouldTriggerCypress }} steps: # For pull requests it's not necessary to check out the code - uses: dorny/paths-filter@v2 id: changes with: filters: | shouldTriggerCypress: # Avoid running E2E tests for irrelevant changes # NOTE: we are at risk of missing a dependency here. We could make # the dependencies more clear if we separated the backend/frontend # code completely - 'ee/**' - 'posthog/**' - 'bin/*' - frontend/**/* - requirements.txt - requirements-dev.txt - package.json - pnpm-lock.yaml # Make sure we run if someone is explicitly change the workflow - .github/workflows/ci-e2e.yml - .github/actions/build-n-cache-image/action.yml # We use docker compose for tests, make sure we rerun on # changes to docker-compose.dev.yml e.g. dependency # version changes - docker-compose.dev.yml - Dockerfile - cypress/** # Job that lists and chunks spec file names and caches node modules chunks: needs: changes name: Cypress preparation runs-on: ubuntu-latest timeout-minutes: 5 outputs: chunks: ${{ steps.chunk.outputs.chunks }} steps: - name: Check out uses: actions/checkout@v3 - name: Group spec files into chunks of three id: chunk run: echo "chunks=$(ls cypress/e2e/* | jq --slurp --raw-input -c 'split("\n")[:-1] | _nwise(2) | join("\n")' | jq --slurp -c .)" >> $GITHUB_OUTPUT container: name: Build and cache container image runs-on: ubuntu-latest timeout-minutes: 60 needs: [changes] permissions: contents: read id-token: write # allow issuing OIDC tokens for this workflow run outputs: tag: ${{ steps.build.outputs.tag }} build-id: ${{ steps.build.outputs.build-id }} steps: - name: Checkout if: needs.changes.outputs.shouldTriggerCypress == 'true' uses: actions/checkout@v3 - name: Build the Docker image with Depot if: needs.changes.outputs.shouldTriggerCypress == 'true' # Build the container image in preparation for the E2E tests uses: ./.github/actions/build-n-cache-image id: build with: save: true actions-id-token-request-url: ${{ env.ACTIONS_ID_TOKEN_REQUEST_URL }} cypress: name: Cypress E2E tests (${{ strategy.job-index }}) runs-on: ubuntu-latest timeout-minutes: 60 needs: [chunks, changes, container] permissions: id-token: write # allow issuing OIDC tokens for this workflow run strategy: # when one test fails, DO NOT cancel the other # containers, as there may be other spec failures # we want to know about. fail-fast: false matrix: chunk: ${{ fromJson(needs.chunks.outputs.chunks) }} steps: - name: Checkout if: needs.changes.outputs.shouldTriggerCypress == 'true' uses: actions/checkout@v3 - name: Install pnpm if: needs.changes.outputs.shouldTriggerCypress == 'true' uses: pnpm/action-setup@v4 - name: Set up Node.js if: needs.changes.outputs.shouldTriggerCypress == 'true' uses: actions/setup-node@v4 with: node-version: 18.12.1 - name: Get pnpm cache directory path if: needs.changes.outputs.shouldTriggerCypress == 'true' id: pnpm-cache-dir run: echo "PNPM_STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT - name: Get cypress cache directory path if: needs.changes.outputs.shouldTriggerCypress == 'true' id: cypress-cache-dir run: echo "CYPRESS_BIN_PATH=$(npx cypress cache path)" >> $GITHUB_OUTPUT - uses: actions/cache@v4 if: needs.changes.outputs.shouldTriggerCypress == 'true' id: pnpm-cache with: path: | ${{ steps.pnpm-cache-dir.outputs.PNPM_STORE_PATH }} ${{ steps.cypress-cache-dir.outputs.CYPRESS_BIN_PATH }} key: ${{ runner.os }}-pnpm-cypress-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-cypress- - name: Install package.json dependencies with pnpm if: needs.changes.outputs.shouldTriggerCypress == 'true' run: pnpm install --frozen-lockfile - name: Stop/Start stack with Docker Compose # these are required checks so, we can't skip entire sections if: needs.changes.outputs.shouldTriggerCypress == 'true' run: | docker compose -f docker-compose.dev.yml down docker compose -f docker-compose.dev.yml up -d - name: Wait for ClickHouse # these are required checks so, we can't skip entire sections if: needs.changes.outputs.shouldTriggerCypress == 'true' run: ./bin/check_kafka_clickhouse_up - name: Install Depot CLI if: needs.changes.outputs.shouldTriggerCypress == 'true' uses: depot/setup-action@v1 - name: Get Docker image cached in Depot if: needs.changes.outputs.shouldTriggerCypress == 'true' uses: depot/pull-action@v1 with: build-id: ${{ needs.container.outputs.build-id }} tags: ${{ needs.container.outputs.tag }} - name: Write .env # This step intentionally has no if, so that GH always considers the action as having run run: | cat <> .env SECRET_KEY=6b01eee4f945ca25045b5aab440b953461faf08693a9abbf1166dc7c6b9772da REDIS_URL=redis://localhost DATABASE_URL=postgres://posthog:posthog@localhost:5432/posthog KAFKA_HOSTS=kafka:9092 DISABLE_SECURE_SSL_REDIRECT=1 SECURE_COOKIES=0 OPT_OUT_CAPTURE=0 E2E_TESTING=1 SKIP_SERVICE_VERSION_REQUIREMENTS=1 EMAIL_HOST=email.test.posthog.net SITE_URL=http://localhost:8000 NO_RESTART_LOOP=1 CLICKHOUSE_SECURE=0 OBJECT_STORAGE_ENABLED=1 OBJECT_STORAGE_ENDPOINT=http://localhost:19000 OBJECT_STORAGE_ACCESS_KEY_ID=object_storage_root_user OBJECT_STORAGE_SECRET_ACCESS_KEY=object_storage_root_password GITHUB_ACTION_RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" CELERY_METRICS_PORT=8999 CLOUD_DEPLOYMENT=E2E ENCRYPTION_SALT_KEYS=00beef0000beef0000beef0000beef00 EOT - name: Start PostHog # these are required checks so, we can't skip entire sections if: needs.changes.outputs.shouldTriggerCypress == 'true' run: | mkdir -p /tmp/logs echo "Starting PostHog using the container image ${{ needs.container.outputs.tag }}" DOCKER_RUN="docker run --rm --network host --add-host kafka:127.0.0.1 --env-file .env ${{ needs.container.outputs.tag }}" $DOCKER_RUN ./bin/migrate $DOCKER_RUN python manage.py setup_dev # only starts the plugin server so that the "wait for PostHog" step passes $DOCKER_RUN ./bin/docker-worker &> /tmp/logs/worker.txt & $DOCKER_RUN ./bin/docker-server &> /tmp/logs/server.txt & - name: Wait for PostHog # these are required checks so, we can't skip entire sections if: needs.changes.outputs.shouldTriggerCypress == 'true' # this action might be abandoned - but v1 doesn't point to latest of v1 (which it should) # so pointing to v1.1.0 to remove warnings about node version with v1 # todo check https://github.com/iFaxity/wait-on-action/releases for new releases uses: iFaxity/wait-on-action@v1.2.1 timeout-minutes: 3 with: verbose: true log: true resource: http://localhost:8000 - name: Cypress run # these are required checks so, we can't skip entire sections if: needs.changes.outputs.shouldTriggerCypress == 'true' uses: cypress-io/github-action@v6 with: config-file: cypress.e2e.config.ts config: retries=2 spec: ${{ matrix.chunk }} install: false # We were seeing suprising crashes in headless mode # See https://github.com/cypress-io/cypress/issues/28893#issuecomment-1956480875 headed: true env: E2E_TESTING: 1 OPT_OUT_CAPTURE: 0 GITHUB_ACTION_RUN_URL: '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}' - name: Archive test screenshots uses: actions/upload-artifact@v3 with: name: screenshots path: cypress/screenshots if: ${{ failure() }} - name: Archive test downloads uses: actions/upload-artifact@v3 with: name: downloads path: cypress/downloads if: ${{ failure() }} - name: Archive test videos uses: actions/upload-artifact@v3 with: name: videos path: cypress/videos if: ${{ failure() }} - name: Archive accessibility violations if: needs.changes.outputs.shouldTriggerCypress == 'true' uses: actions/upload-artifact@v3 with: name: accessibility-violations path: '**/a11y/' if-no-files-found: 'ignore' - name: Show logs on failure # use artefact here, as I think the output will be too large for display in an action uses: actions/upload-artifact@v3 with: name: logs-${{ strategy.job-index }} path: /tmp/logs if: ${{ failure() }} calculate-running-time: name: Calculate running time runs-on: ubuntu-latest needs: [cypress] if: needs.changes.outputs.shouldTriggerCypress == 'true' && github.event.pull_request.head.repo.full_name == 'PostHog/posthog' steps: - name: Calculate running time run: | echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token run_id=${GITHUB_RUN_ID} repo=${GITHUB_REPOSITORY} run_info=$(gh api repos/${repo}/actions/runs/${run_id}) echo run_info: ${run_info} # name is the name of the workflow file # run_started_at is the start time of the workflow # we want to get the number of seconds between the start time and now name=$(echo ${run_info} | jq -r '.name') run_url=$(echo ${run_info} | jq -r '.url') run_started_at=$(echo ${run_info} | jq -r '.run_started_at') run_attempt=$(echo ${run_info} | jq -r '.run_attempt') start_seconds=$(date -d "${run_started_at}" +%s) now_seconds=$(date +%s) duration=$((now_seconds-start_seconds)) echo running_time_duration_seconds=${duration} >> $GITHUB_ENV echo running_time_run_url=${run_url} >> $GITHUB_ENV echo running_time_run_attempt=${run_attempt} >> $GITHUB_ENV echo running_time_run_id=${run_id} >> $GITHUB_ENV echo running_time_run_started_at=${run_started_at} >> $GITHUB_ENV - name: Capture running time to PostHog if: github.event.pull_request.head.repo.full_name == 'PostHog/posthog' uses: PostHog/posthog-github-action@v0.1 with: posthog-token: ${{secrets.POSTHOG_API_TOKEN}} event: 'posthog-ci-running-time' properties: '{"duration_seconds": ${{ env.running_time_duration_seconds }}, "run_url": "${{ env.running_time_run_url }}", "run_attempt": "${{ env.running_time_run_attempt }}", "run_id": "${{ env.running_time_run_id }}", "run_started_at": "${{ env.running_time_run_started_at }}"}'