diff --git a/.github/workflows/build-client-server.yml b/.github/workflows/build-client-server.yml
index ea090a91ea..cd97d37cc0 100644
--- a/.github/workflows/build-client-server.yml
+++ b/.github/workflows/build-client-server.yml
@@ -186,45 +186,63 @@ jobs:
run:
shell: bash
steps:
- # Deleting the existing dir's if any
- - name: Delete existing directories
+ - name: Setup node
if: needs.ci-test-limited.result != 'success'
- run: |
- rm -f ~/failed_spec_ci
- rm -f ~/combined_failed_spec_ci
-
- # Force store previous cypress dashboard url from cache
- - name: Store the previous cypress dashboard url
- if: success()
- uses: actions/cache@v3
+ uses: actions/setup-node@v3
with:
- path: |
- ~/cypress_url
- key: ${{ github.run_id }}-dashboard-url-${{ github.run_attempt }}
- restore-keys: |
- ${{ github.run_id }}-dashboard-url
+ node-version: 18
- - name: Print cypress dashboard url
- id: dashboard_url
- run: |
- cypress_url="https://internal.appsmith.com/app/cypressdashboard/rundetails-64ec3df0c632e24c00764938?branch=master&workflowId=${{ github.run_id }}&attempt=${{ github.run_attempt }}"
- echo "dashboard_url=$cypress_url" >> $GITHUB_OUTPUT
-
- # Download failed_spec list for all jobs
- - uses: actions/download-artifact@v3
+ - name: install pg
if: needs.ci-test-limited.result != 'success'
- id: download_ci
+ run : npm install pg
+
+ - name: Fetch the failed specs
+ if: needs.ci-test-limited.result != 'success'
+ id: failed_specs
+ env:
+ DB_HOST: ${{ secrets.CYPRESS_DB_HOST }}
+ DB_NAME: ${{ secrets.CYPRESS_DB_NAME }}
+ DB_USER: ${{ secrets.CYPRESS_DB_USER }}
+ DB_PWD: ${{ secrets.CYPRESS_DB_PWD }}
+ RUN_ID: ${{ github.run_id }}
+ ATTEMPT_NUMBER: ${{ github.run_attempt }}
+ uses: actions/github-script@v6
with:
- name: failed-spec-ci-${{github.run_attempt}}
- path: ~/failed_spec_ci
+ script: |
+ const { Pool } = require("pg");
+ const { DB_HOST, DB_NAME, DB_USER, DB_PWD, RUN_ID, ATTEMPT_NUMBER } = process.env
+
+ const client = await new Pool({
+ user: DB_USER,
+ host: DB_HOST,
+ database: DB_NAME,
+ password: DB_PWD,
+ port: 5432,
+ connectionTimeoutMillis: 60000,
+ }).connect();
+
+ const result = await client.query(
+ `SELECT DISTINCT name FROM public."specs"
+ WHERE "matrixId" IN
+ (SELECT id FROM public."matrix"
+ WHERE "attemptId" = (
+ SELECT id FROM public."attempt" WHERE "workflowId" = $1 and "attempt" = $2
+ )
+ ) AND status = 'fail'`,
+ [RUN_ID, ATTEMPT_NUMBER],
+ );
+ client.release();
+ return result.rows.map((spec) => spec.name);
# In case for any ci job failure, create combined failed spec
- - name: "combine all specs for CI"
+ - name: combine all specs for CI
+ id: combine_ci
if: needs.ci-test-limited.result != 'success'
run: |
- echo "Debugging: failed specs in ~/failed_spec_ci/failed_spec_ci*"
- cat ~/failed_spec_ci/failed_spec_ci*
- cat ~/failed_spec_ci/failed_spec_ci* | sort -u >> ~/combined_failed_spec_ci
+ failed_specs=$(echo ${{steps.test.outputs.result}} | sed 's/\[\|\]//g' | tr -d ' ' | tr ',' '\n')
+ while read -r line; do
+ echo "$line" >> ~/combined_failed_spec_ci
+ done <<< "$failed_specs"
# Upload combined failed CI spec list to a file
# This is done for debugging.
@@ -285,45 +303,63 @@ jobs:
run:
shell: bash
steps:
- # Deleting the existing dir's if any
- - name: Delete existing directories
+ - name: Setup node
if: needs.ci-test-limited-existing-docker-image.result != 'success'
- run: |
- rm -f ~/failed_spec_ci
- rm -f ~/combined_failed_spec_ci
-
- # Force store previous cypress dashboard url from cache
- - name: Store the previous cypress dashboard url
- if: success()
- uses: actions/cache@v3
+ uses: actions/setup-node@v3
with:
- path: |
- ~/cypress_url
- key: ${{ github.run_id }}-dashboard-url-${{ github.run_attempt }}
- restore-keys: |
- ${{ github.run_id }}-dashboard-url
+ node-version: 18
- - name: Print cypress dashboard url
- id: dashboard_url
- run: |
- cypress_url="https://internal.appsmith.com/app/cypressdashboard/rundetails-64ec3df0c632e24c00764938?branch=master&workflowId=${{ github.run_id }}&attempt=${{ github.run_attempt }}"
- echo "dashboard_url=$cypress_url" >> $GITHUB_OUTPUT
-
- # Download failed_spec list for all jobs
- - uses: actions/download-artifact@v3
+ - name: install pg
if: needs.ci-test-limited-existing-docker-image.result != 'success'
- id: download_ci
+ run : npm install pg
+
+ - name: Fetch the failed specs
+ if: needs.ci-test-limited-existing-docker-image.result != 'success'
+ id: failed_specs
+ env:
+ DB_HOST: ${{ secrets.CYPRESS_DB_HOST }}
+ DB_NAME: ${{ secrets.CYPRESS_DB_NAME }}
+ DB_USER: ${{ secrets.CYPRESS_DB_USER }}
+ DB_PWD: ${{ secrets.CYPRESS_DB_PWD }}
+ RUN_ID: ${{ github.run_id }}
+ ATTEMPT_NUMBER: ${{ github.run_attempt }}
+ uses: actions/github-script@v6
with:
- name: failed-spec-ci-${{github.run_attempt}}
- path: ~/failed_spec_ci
+ script: |
+ const { Pool } = require("pg");
+ const { DB_HOST, DB_NAME, DB_USER, DB_PWD, RUN_ID, ATTEMPT_NUMBER } = process.env
+
+ const client = await new Pool({
+ user: DB_USER,
+ host: DB_HOST,
+ database: DB_NAME,
+ password: DB_PWD,
+ port: 5432,
+ connectionTimeoutMillis: 60000,
+ }).connect();
+
+ const result = await client.query(
+ `SELECT DISTINCT name FROM public."specs"
+ WHERE "matrixId" IN
+ (SELECT id FROM public."matrix"
+ WHERE "attemptId" = (
+ SELECT id FROM public."attempt" WHERE "workflowId" = $1 and "attempt" = $2
+ )
+ ) AND status = 'fail'`,
+ [RUN_ID, ATTEMPT_NUMBER],
+ );
+ client.release();
+ return result.rows.map((spec) => spec.name);
# In case for any ci job failure, create combined failed spec
- - name: "combine all specs for CI"
+ - name: combine all specs for CI
+ id: combine_ci
if: needs.ci-test-limited-existing-docker-image.result != 'success'
run: |
- echo "Debugging: failed specs in ~/failed_spec_ci/failed_spec_ci*"
- cat ~/failed_spec_ci/failed_spec_ci*
- cat ~/failed_spec_ci/failed_spec_ci* | sort -u >> ~/combined_failed_spec_ci
+ failed_specs=$(echo ${{steps.test.outputs.result}} | sed 's/\[\|\]//g' | tr -d ' ' | tr ',' '\n')
+ while read -r line; do
+ echo "$line" >> ~/combined_failed_spec_ci
+ done <<< "$failed_specs"
# Upload combined failed CI spec list to a file
# This is done for debugging.
diff --git a/.github/workflows/integration-tests-command.yml b/.github/workflows/integration-tests-command.yml
index 2108fbcb6c..9390647800 100644
--- a/.github/workflows/integration-tests-command.yml
+++ b/.github/workflows/integration-tests-command.yml
@@ -95,48 +95,63 @@ jobs:
PAYLOAD_CONTEXT: ${{ toJson(github.event.client_payload) }}
run: echo "$PAYLOAD_CONTEXT"
- # Deleting the existing dir's if any
- - name: Delete existing directories
+ - name: Setup node
if: needs.ci-test.result != 'success'
- run: |
- rm -f ~/failed_spec_ci
- rm -f ~/combined_failed_spec_ci
-
- # Force store previous cypress dashboard url from cache
- - name: Store the previous cypress dashboard url
- continue-on-error: true
- if: success()
- uses: actions/cache@v3
+ uses: actions/setup-node@v3
with:
- path: |
- ~/cypress_url
- key: ${{ github.run_id }}-dashboard-url-${{ github.run_attempt }}
- restore-keys: |
- ${{ github.run_id }}-dashboard-url
+ node-version: 18
- - name: Print cypress dashboard url
- continue-on-error: true
- id: dashboard_url
- run: |
- cypress_url="https://internal.appsmith.com/app/cypressdashboard/rundetails-64ec3df0c632e24c00764938?branch=master&workflowId=${{ github.run_id }}&attempt=${{ github.run_attempt }}"
- echo "dashboard_url=$cypress_url" >> $GITHUB_OUTPUT
-
- # Download failed_spec list for all jobs
- - uses: actions/download-artifact@v3
+ - name: install pg
if: needs.ci-test.result != 'success'
- id: download_ci
+ run : npm install pg
+
+ - name: Fetch the failed specs
+ if: needs.ci-test.result != 'success'
+ id: failed_specs
+ env:
+ DB_HOST: ${{ secrets.CYPRESS_DB_HOST }}
+ DB_NAME: ${{ secrets.CYPRESS_DB_NAME }}
+ DB_USER: ${{ secrets.CYPRESS_DB_USER }}
+ DB_PWD: ${{ secrets.CYPRESS_DB_PWD }}
+ RUN_ID: ${{ github.run_id }}
+ ATTEMPT_NUMBER: ${{ github.run_attempt }}
+ uses: actions/github-script@v6
with:
- name: failed-spec-ci-${{github.run_attempt}}
- path: ~/failed_spec_ci
+ script: |
+ const { Pool } = require("pg");
+ const { DB_HOST, DB_NAME, DB_USER, DB_PWD, RUN_ID, ATTEMPT_NUMBER } = process.env
+
+ const client = await new Pool({
+ user: DB_USER,
+ host: DB_HOST,
+ database: DB_NAME,
+ password: DB_PWD,
+ port: 5432,
+ connectionTimeoutMillis: 60000,
+ }).connect();
+
+ const result = await client.query(
+ `SELECT DISTINCT name FROM public."specs"
+ WHERE "matrixId" IN
+ (SELECT id FROM public."matrix"
+ WHERE "attemptId" = (
+ SELECT id FROM public."attempt" WHERE "workflowId" = $1 and "attempt" = $2
+ )
+ ) AND status = 'fail'`,
+ [RUN_ID, ATTEMPT_NUMBER],
+ );
+ client.release();
+ return result.rows.map((spec) => spec.name);
# In case for any ci job failure, create combined failed spec
- name: combine all specs for CI
id: combine_ci
if: needs.ci-test.result != 'success'
run: |
- echo "Debugging: failed specs in ~/failed_spec_ci/failed_spec_ci*"
- cat ~/failed_spec_ci/failed_spec_ci*
- cat ~/failed_spec_ci/failed_spec_ci* | sort -u >> ~/combined_failed_spec_ci
+ failed_specs=$(echo ${{steps.test.outputs.result}} | sed 's/\[\|\]//g' | tr -d ' ' | tr ',' '\n')
+ while read -r line; do
+ echo "$line" >> ~/combined_failed_spec_ci
+ done <<< "$failed_specs"
if [[ -z $(grep '[^[:space:]]' ~/combined_failed_spec_ci) ]] ; then
echo "specs_failed=0" >> $GITHUB_OUTPUT
else
@@ -156,7 +171,7 @@ jobs:
shell: bash
run: |
curl --request POST --url https://yatin-s-workspace-jk8ru5.us-east-1.xata.sh/db/CypressKnownFailures:main/tables/CypressKnownFailuires/query --header 'Authorization: Bearer ${{ secrets.XATA_TOKEN }}' --header 'Content-Type: application/json'|jq -r |grep Spec|cut -d ':' -f 2 2> /dev/null|sed 's/"//g'|sed 's/,//g' > ~/knownfailures
-
+
# Verify CI test failures against known failures
- name: Verify CI test failures against known failures
if: needs.ci-test.result != 'success'
@@ -181,7 +196,18 @@ jobs:
To know the list of identified flaky tests - Refer here
- name: Add a comment on the PR when ci-test is success
- if: needs.ci-test.result == 'success' || steps.combine_ci.outputs.specs_failed == '0'
+ if: needs.ci-test.result != 'success' && steps.combine_ci.outputs.specs_failed == '0'
+ uses: peter-evans/create-or-update-comment@v1
+ with:
+ issue-number: ${{ github.event.client_payload.pull_request.number }}
+ body: |
+ Workflow run: <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}>.
+ Commit: `${{ github.event.client_payload.slash_command.args.named.sha }}`.
+ Cypress dashboard url: Click here!
+ It seems like there are some failures 😔. We are not able to recognize it, please check this manually here.
+
+ - name: Add a comment on the PR when ci-test is success
+ if: needs.ci-test.result == 'success' && steps.combine_ci.outputs.specs_failed == '0'
uses: peter-evans/create-or-update-comment@v1
with:
issue-number: ${{ github.event.client_payload.pull_request.number }}
diff --git a/app/client/cypress/cypress-split.ts b/app/client/cypress/cypress-split.ts
deleted file mode 100644
index 2b6fbace72..0000000000
--- a/app/client/cypress/cypress-split.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-/* eslint-disable no-console */
-import globby from "globby";
-import minimatch from "minimatch";
-import { exec } from "child_process";
-
-const fs = require("fs/promises");
-
-type GetEnvOptions = {
- required?: boolean;
-};
-
-// used to roughly determine how many tests are in a file
-const testPattern = /(^|\s)(it)\(/g;
-
-// This function will get all the spec paths using the pattern
-async function getSpecFilePaths(
- specPattern: any,
- ignoreTestFiles: any,
-): Promise {
- const files = globby.sync(specPattern, {
- ignore: ignoreTestFiles,
- });
-
- // ignore the files that doesn't match
- const ignorePatterns = [...(ignoreTestFiles || [])];
-
- // a function which returns true if the file does NOT match
- const doesNotMatchAllIgnoredPatterns = (file: string) => {
- // using {dot: true} here so that folders with a '.' in them are matched
- const MINIMATCH_OPTIONS = { dot: true, matchBase: true };
- return ignorePatterns.every((pattern) => {
- return !minimatch(file, pattern, MINIMATCH_OPTIONS);
- });
- };
- const filtered = files.filter(doesNotMatchAllIgnoredPatterns);
- return filtered;
-}
-
-// This function will determine the test counts in each file to sort it further
-async function getTestCount(filePath: string): Promise {
- const content = await fs.readFile(filePath, "utf8");
- return content.match(testPattern)?.length || 0;
-}
-
-// Sorting the spec files as per the test count in it
-async function sortSpecFilesByTestCount(
- specPathsOriginal: string[],
-): Promise {
- const specPaths = [...specPathsOriginal];
-
- const testPerSpec: Record = {};
-
- for (const specPath of specPaths) {
- testPerSpec[specPath] = await getTestCount(specPath);
- }
-
- return (
- Object.entries(testPerSpec)
- // Sort by the number of tests per spec file. And this will create a consistent file list/ordering so that file division proper.
- .sort((a, b) => b[1] - a[1])
- .map((x) => x[0])
- );
-}
-
-// This function will split the specs between the runners by calculating the modulus between spec index and the totalRunners
-function splitSpecs(
- specs: string[],
- totalRunnersCount: number,
- currentRunner: number,
-): string[] {
- let specs_to_run = specs.filter((_, index) => {
- return index % totalRunnersCount === currentRunner;
- });
- return specs_to_run;
-}
-
-// This function will finally get the specs as a comma separated string to pass the specs to the command
-async function getSpecsToRun(
- totalRunnersCount = 0,
- currentRunner = 0,
- specPattern: string | string[] = "cypress/e2e/**/**/*.{js,ts}",
- ignorePattern: string | string[],
-): Promise {
- try {
- const specFilePaths = await sortSpecFilesByTestCount(
- await getSpecFilePaths(specPattern, ignorePattern),
- );
-
- if (!specFilePaths.length) {
- throw Error("No spec files found.");
- }
- const specsToRun = splitSpecs(
- specFilePaths,
- totalRunnersCount,
- currentRunner,
- );
- return specsToRun;
- } catch (err) {
- console.error(err);
- process.exit(1);
- }
-}
-
-// This function will help to get and convert the env variables
-function getEnvNumber(
- varName: string,
- { required = false }: GetEnvOptions = {},
-): number {
- if (required && process.env[varName] === undefined) {
- throw Error(`${varName} is not set.`);
- }
- const value = Number(process.env[varName]);
- if (isNaN(value)) {
- throw Error(`${varName} is not a number.`);
- }
- return value;
-}
-
-// This function will helps to check and get env variables
-function getEnvValue(
- varName: string,
- { required = false }: GetEnvOptions = {},
-) {
- if (required && process.env[varName] === undefined) {
- throw Error(`${varName} is not set.`);
- }
- const value = process.env[varName] === undefined ? "" : process.env[varName];
- return value;
-}
-
-// This is to fetch the env variables from CI
-function getArgs() {
- return {
- totalRunners: getEnvValue("TOTAL_RUNNERS", { required: false }),
- thisRunner: getEnvValue("THIS_RUNNER", { required: false }),
- cypressSpecs: getEnvValue("CYPRESS_SPECS", { required: false }),
- };
-}
-
-export async function cypressSplit(on: any, config: any) {
- try {
- let currentRunner = 0;
- let allRunners = 1;
- let specPattern = await config.specPattern;
- const ignorePattern = await config.excludeSpecPattern;
- const { cypressSpecs, thisRunner, totalRunners } = getArgs();
-
- if (cypressSpecs != "")
- specPattern = cypressSpecs?.split(",").filter((val) => val !== "");
-
- if (totalRunners != "") {
- currentRunner = Number(thisRunner);
- allRunners = Number(totalRunners);
- }
-
- const specs = await getSpecsToRun(
- allRunners,
- currentRunner,
- specPattern,
- ignorePattern,
- );
-
- if (specs.length > 0) {
- config.specPattern = specs.length == 1 ? specs[0] : specs;
- } else {
- config.specPattern = "cypress/scripts/no_spec.ts";
- }
- return config;
- } catch (err) {
- console.log(err);
- }
-}
diff --git a/app/client/cypress/plugins/index.js b/app/client/cypress/plugins/index.js
index d24806da51..2f24408f1e 100644
--- a/app/client/cypress/plugins/index.js
+++ b/app/client/cypress/plugins/index.js
@@ -12,7 +12,7 @@ const {
} = require("cypress-image-snapshot/plugin");
const { tagify } = require("cypress-tags");
const { cypressHooks } = require("../scripts/cypress-hooks");
-const { cypressSplit } = require("../cypress-split");
+const { cypressSplit } = require("../scripts/cypress-split");
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
@@ -216,7 +216,7 @@ module.exports = async (on, config) => {
});
if (process.env["RUNID"]) {
- config = await cypressSplit(on, config);
+ config = await new cypressSplit().splitSpecs(on, config);
cypressHooks(on, config);
}
diff --git a/app/client/cypress/scripts/cypress-hooks.js b/app/client/cypress/scripts/cypress-hooks.js
deleted file mode 100644
index fb509f6516..0000000000
--- a/app/client/cypress/scripts/cypress-hooks.js
+++ /dev/null
@@ -1,244 +0,0 @@
-const { Pool } = require("pg");
-const os = require("os");
-const AWS = require("aws-sdk");
-const fs = require("fs");
-
-exports.cypressHooks = cypressHooks;
-
-// This function will helps to check and get env variables
-function getEnvValue(varName, { required = true }) {
- if (required && process.env[varName] === undefined) {
- throw Error(
- `Please check some or all the following ENV variables are not set properly [ RUNID, ATTEMPT_NUMBER, REPOSITORY, COMMITTER, TAG, BRANCH, THIS_RUNNER, CYPRESS_DB_USER, CYPRESS_DB_HOST, CYPRESS_DB_NAME, CYPRESS_DB_PWD, CYPRESS_S3_ACCESS, CYPRESS_S3_SECRET ].`,
- );
- }
- const value =
- process.env[varName] === undefined ? "Cypress test" : process.env[varName];
- return value;
-}
-
-//This is to setup the db client
-function configureDbClient() {
- const dbConfig = {
- user: getEnvValue("CYPRESS_DB_USER", { required: true }),
- host: getEnvValue("CYPRESS_DB_HOST", { required: true }),
- database: getEnvValue("CYPRESS_DB_NAME", { required: true }),
- password: getEnvValue("CYPRESS_DB_PWD", { required: true }),
- port: 5432,
- connectionTimeoutMillis: 60000,
- ssl: true,
- keepalives: 0,
- };
-
- const dbClient = new Pool(dbConfig);
-
- return dbClient;
-}
-
-// This is to setup the AWS client
-function configureS3() {
- AWS.config.update({ region: "ap-south-1" });
- const s3client = new AWS.S3({
- credentials: {
- accessKeyId: getEnvValue("CYPRESS_S3_ACCESS", { required: true }),
- secretAccessKey: getEnvValue("CYPRESS_S3_SECRET", { required: true }),
- },
- });
- return s3client;
-}
-
-// This is to upload files to s3 when required
-function uploadToS3(s3Client, filePath, key) {
- const fileContent = fs.readFileSync(filePath);
-
- const params = {
- Bucket: "appsmith-internal-cy-db",
- Key: key,
- Body: fileContent,
- };
- return s3Client.upload(params).promise();
-}
-
-async function cypressHooks(on, config) {
- const s3 = configureS3();
- const runData = {
- commitMsg: getEnvValue("COMMIT_INFO_MESSAGE", { required: false }),
- workflowId: getEnvValue("RUNID", { required: true }),
- attempt: getEnvValue("ATTEMPT_NUMBER", { required: true }),
- os: os.type(),
- repo: getEnvValue("REPOSITORY", { required: true }),
- committer: getEnvValue("COMMITTER", { required: true }),
- type: getEnvValue("TAG", { required: true }),
- branch: getEnvValue("BRANCH", { required: true }),
- };
- const matrix = {
- matrixId: getEnvValue("THIS_RUNNER", { required: true }),
- matrixStatus: "started",
- };
-
- const specData = {};
-
- await on("before:run", async (runDetails) => {
- runData.browser = runDetails.browser.name;
- const dbClient = await configureDbClient().connect();
- try {
- const runResponse = await dbClient.query(
- `INSERT INTO public.attempt ("workflowId", "attempt", "browser", "os", "repo", "committer", "type", "commitMsg", "branch")
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
- ON CONFLICT ("workflowId", attempt) DO NOTHING
- RETURNING id;`,
- [
- runData.workflowId,
- runData.attempt,
- runData.browser,
- runData.os,
- runData.repo,
- runData.committer,
- runData.type,
- runData.commitMsg,
- runData.branch,
- ],
- );
-
- if (runResponse.rows.length > 0) {
- runData.attemptId = runResponse.rows[0].id; // Save the inserted attempt ID for later updates
- } else {
- const res = await dbClient.query(
- `SELECT id FROM public.attempt WHERE "workflowId" = $1 AND attempt = $2`,
- [runData.workflowId, runData.attempt],
- );
- runData.attemptId = res.rows[0].id;
- }
-
- const matrixResponse = await dbClient.query(
- `INSERT INTO public.matrix ("workflowId", "matrixId", "status", "attemptId")
- VALUES ($1, $2, $3, $4)
- ON CONFLICT ("matrixId", "attemptId") DO NOTHING
- RETURNING id;`,
- [
- runData.workflowId,
- matrix.matrixId,
- matrix.matrixStatus,
- runData.attemptId,
- ],
- );
- matrix.id = matrixResponse.rows[0].id; // Save the inserted matrix ID for later updates
- } catch (err) {
- console.log(err);
- } finally {
- await dbClient.release();
- }
- });
-
- await on("before:spec", async (spec) => {
- specData.name = spec.relative;
- specData.matrixId = matrix.id;
- const dbClient = await configureDbClient().connect();
- try {
- if (!specData.name.includes("no_spec.ts")) {
- const specResponse = await dbClient.query(
- 'INSERT INTO public.specs ("name", "matrixId") VALUES ($1, $2) RETURNING id',
- [specData.name, matrix.id],
- );
- specData.specId = specResponse.rows[0].id; // Save the inserted spec ID for later updates
- }
- } catch (err) {
- console.log(err);
- } finally {
- await dbClient.release();
- }
- });
-
- await on("after:spec", async (spec, results) => {
- specData.testCount = results.stats.tests;
- specData.passes = results.stats.passes;
- specData.failed = results.stats.failures;
- specData.pending = results.stats.pending;
- specData.skipped = results.stats.skipped;
- specData.status = results.stats.failures > 0 ? "fail" : "pass";
- specData.duration = results.stats.wallClockDuration;
-
- const dbClient = await configureDbClient().connect();
- try {
- if (!specData.name.includes("no_spec.ts")) {
- await dbClient.query(
- 'UPDATE public.specs SET "testCount" = $1, "passes" = $2, "failed" = $3, "skipped" = $4, "pending" = $5, "status" = $6, "duration" = $7 WHERE id = $8',
- [
- results.stats.tests,
- results.stats.passes,
- results.stats.failures,
- results.stats.skipped,
- results.stats.pending,
- specData.status,
- specData.duration,
- specData.specId,
- ],
- );
- for (const test of results.tests) {
- const testResponse = await dbClient.query(
- `INSERT INTO public.tests ("name", "specId", "status", "retries", "retryData") VALUES ($1, $2, $3, $4, $5) RETURNING id`,
- [
- test.title[1],
- specData.specId,
- test.state,
- test.attempts.length,
- JSON.stringify(test.attempts),
- ],
- );
- if (
- test.attempts.some((attempt) => attempt.state === "failed") &&
- results.screenshots
- ) {
- const out = results.screenshots.filter(
- (scr) => scr.testId === test.testId,
- );
- console.log("Uploading screenshots...");
- for (const scr of out) {
- const key = `${testResponse.rows[0].id}_${specData.specId}_${
- scr.testAttemptIndex + 1
- }`;
- Promise.all([uploadToS3(s3, scr.path, key)]).catch((error) => {
- console.log("Error in uploading screenshots:", error);
- });
- }
- }
- }
-
- if (
- results.tests.some((test) =>
- test.attempts.some((attempt) => attempt.state === "failed"),
- ) &&
- results.video
- ) {
- console.log("Uploading video...");
- const key = `${specData.specId}`;
- Promise.all([uploadToS3(s3, results.video, key)]).catch((error) => {
- console.log("Error in uploading video:", error);
- });
- }
- }
- } catch (err) {
- console.log(err);
- } finally {
- await dbClient.release();
- }
- });
-
- on("after:run", async (runDetails) => {
- const dbClient = await configureDbClient().connect();
- try {
- await dbClient.query(
- `UPDATE public.matrix SET "status" = $1 WHERE id = $2`,
- ["done", matrix.id],
- );
- await dbClient.query(
- `UPDATE public.attempt SET "endTime" = $1 WHERE "id" = $2`,
- [new Date(), runData.attemptId],
- );
- } catch (err) {
- console.log(err);
- } finally {
- await dbClient.end();
- }
- });
-}
diff --git a/app/client/cypress/scripts/cypress-hooks.ts b/app/client/cypress/scripts/cypress-hooks.ts
new file mode 100644
index 0000000000..50a1dbc189
--- /dev/null
+++ b/app/client/cypress/scripts/cypress-hooks.ts
@@ -0,0 +1,147 @@
+import os from "os";
+import util from "./util";
+
+export async function cypressHooks(
+ on: Cypress.PluginEvents,
+ config: Cypress.PluginConfigOptions,
+) {
+ const _ = new util();
+ const s3 = _.configureS3();
+ const dbClient = _.configureDbClient();
+ const runData: any = {
+ workflowId: _.getVars().runId,
+ attempt: _.getVars().attempt_number,
+ os: os.type(),
+ };
+ const matrix: any = {
+ matrixId: _.getVars().thisRunner,
+ matrixStatus: "started",
+ };
+ const specData: any = {};
+
+ on("before:run", async (runDetails: Cypress.BeforeRunDetails) => {
+ runData.browser = runDetails.browser?.name;
+ const client = await dbClient.connect();
+ try {
+ const attemptRes = await client.query(
+ `UPDATE public."attempt" SET "browser" = $1, "os" = $2 WHERE "workflowId" = $3 AND attempt = $4 RETURNING id`,
+ [runData.browser, runData.os, runData.workflowId, runData.attempt],
+ );
+ runData.attemptId = attemptRes.rows[0].id;
+ const matrixRes = await client.query(
+ `SELECT id FROM public."matrix" WHERE "attemptId" = $1 AND "matrixId" = $2`,
+ [runData.attemptId, matrix.matrixId],
+ );
+ matrix.id = matrixRes.rowCount > 0 ? matrixRes.rows[0].id : "";
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ });
+
+ on("before:spec", async (spec: Cypress.Spec) => {
+ specData.name = spec.relative;
+ specData.matrixId = matrix.id;
+ const client = await dbClient.connect();
+ try {
+ if (!specData.name.includes("no_spec.ts")) {
+ const specResponse = await client.query(
+ `UPDATE public."specs" SET "status" = $1 WHERE "name" = $2 AND "matrixId" = $3 RETURNING id`,
+ ["in-progress", specData.name, matrix.id],
+ );
+ specData.specId = specResponse.rows[0].id;
+ }
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ });
+
+ on(
+ "after:spec",
+ async (spec: Cypress.Spec, results: CypressCommandLine.RunResult) => {
+ const client = await dbClient.connect();
+ try {
+ if (!specData.name.includes("no_spec.ts")) {
+ await client.query(
+ 'UPDATE public.specs SET "testCount" = $1, "passes" = $2, "failed" = $3, "skipped" = $4, "pending" = $5, "status" = $6, "duration" = $7 WHERE id = $8',
+ [
+ results.stats.tests,
+ results.stats.passes,
+ results.stats.failures,
+ results.stats.skipped,
+ results.stats.pending,
+ results.stats.failures > 0 ? "fail" : "pass",
+ results.stats.duration,
+ specData.specId,
+ ],
+ );
+ for (const test of results.tests) {
+ const testResponse = await client.query(
+ `INSERT INTO public.tests ("name", "specId", "status", "retries", "retryData") VALUES ($1, $2, $3, $4, $5) RETURNING id`,
+ [
+ test.title[1],
+ specData.specId,
+ test.state,
+ test.attempts.length,
+ test.displayError,
+ ],
+ );
+ if (
+ test.attempts.some((attempt) => attempt.state === "failed") &&
+ results.screenshots.length > 0
+ ) {
+ const out = results.screenshots.filter((scr) =>
+ scr.path.includes(test.title[1]),
+ );
+ console.log("Uploading screenshots...");
+ for (const scr of out) {
+ const attempt = scr.path.includes("attempt 2") ? 2 : 1;
+ const key = `${testResponse.rows[0].id}_${specData.specId}_${attempt}`;
+ await _.uploadToS3(s3, scr.path, key);
+ }
+ }
+ }
+
+ if (
+ results.tests.some((test) =>
+ test.attempts.some((attempt) => attempt.state === "failed"),
+ ) &&
+ results.video
+ ) {
+ console.log("Uploading video...");
+ const key = `${specData.specId}`;
+ await _.uploadToS3(s3, results.video, key);
+ }
+ }
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ },
+ );
+
+ on("after:run", async (runDetails) => {
+ const client = await dbClient.connect();
+ try {
+ if (!specData.name.includes("no_spec.ts")) {
+ await client.query(
+ `UPDATE public.matrix SET "status" = $1 WHERE id = $2`,
+ ["done", matrix.id],
+ );
+ await client.query(
+ `UPDATE public.attempt SET "endTime" = $1 WHERE "id" = $2`,
+ [new Date(), runData.attemptId],
+ );
+ }
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ await dbClient.end();
+ }
+ });
+}
diff --git a/app/client/cypress/scripts/cypress-split.ts b/app/client/cypress/scripts/cypress-split.ts
new file mode 100644
index 0000000000..b44c99970a
--- /dev/null
+++ b/app/client/cypress/scripts/cypress-split.ts
@@ -0,0 +1,325 @@
+/* eslint-disable no-console */
+import type { DataItem } from "./util";
+import util from "./util";
+import globby from "globby";
+import minimatch from "minimatch";
+import cypress from "cypress";
+
+export class cypressSplit {
+ util = new util();
+ dbClient = this.util.configureDbClient();
+
+ // This function will get all the spec paths using the pattern
+ private async getSpecFilePaths(
+ specPattern: any,
+ ignoreTestFiles: any,
+ ): Promise {
+ const files = globby.sync(specPattern, {
+ ignore: ignoreTestFiles,
+ });
+
+ // ignore the files that doesn't match
+ const ignorePatterns = [...(ignoreTestFiles || [])];
+
+ // a function which returns true if the file does NOT match
+ const doesNotMatchAllIgnoredPatterns = (file: string) => {
+ // using {dot: true} here so that folders with a '.' in them are matched
+ const MINIMATCH_OPTIONS = { dot: true, matchBase: true };
+ return ignorePatterns.every((pattern) => {
+ return !minimatch(file, pattern, MINIMATCH_OPTIONS);
+ });
+ };
+ const filtered = files.filter(doesNotMatchAllIgnoredPatterns);
+ return filtered;
+ }
+
+ private async getSpecsWithTime(specs: string[], attemptId: number) {
+ const client = await this.dbClient.connect();
+ const defaultDuration = 180000;
+ const specsMap = new Map();
+ try {
+ const queryRes = await client.query(
+ 'SELECT * FROM public."spec_avg_duration" ORDER BY duration DESC',
+ );
+
+ queryRes.rows.forEach((obj) => {
+ specsMap.set(obj.name, obj);
+ });
+
+ const allSpecsWithDuration = specs.map((spec) => {
+ const match = specsMap.get(spec);
+ return match ? match : { name: spec, duration: defaultDuration };
+ });
+ const activeRunners = await this.util.getActiveRunners();
+ const activeRunnersFromDb = await this.getActiveRunnersFromDb(attemptId);
+ return await this.util.divideSpecsIntoBalancedGroups(
+ allSpecsWithDuration,
+ Number(activeRunners) - Number(activeRunnersFromDb),
+ );
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ }
+
+ // This function will finally get the specs as a comma separated string to pass the specs to the command
+ private async getSpecsToRun(
+ specPattern: string | string[] = "cypress/e2e/**/**/*.{js,ts}",
+ ignorePattern: string | string[],
+ attemptId: number,
+ ): Promise {
+ try {
+ const specFilePaths = await this.getSpecFilePaths(
+ specPattern,
+ ignorePattern,
+ );
+
+ const specsToRun = await this.getSpecsWithTime(specFilePaths, attemptId);
+ return specsToRun === undefined
+ ? []
+ : specsToRun[0].map((spec) => spec.name);
+ } catch (err) {
+ console.error(err);
+ process.exit(1);
+ }
+ }
+
+ private async getActiveRunnersFromDb(attemptId: number) {
+ const client = await this.dbClient.connect();
+ try {
+ const matrixRes = await client.query(
+ `SELECT * FROM public."matrix" WHERE "attemptId" = $1`,
+ [attemptId],
+ );
+ return matrixRes.rowCount;
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ }
+
+ private async getAlreadyRunningSpecs() {
+ const client = await this.dbClient.connect();
+ try {
+ const dbRes = await client.query(
+ `SELECT name FROM public."specs"
+ WHERE "matrixId" IN
+ (SELECT id FROM public."matrix"
+ WHERE "attemptId" = (
+ SELECT id FROM public."attempt" WHERE "workflowId" = $1 and "attempt" = $2
+ )
+ )`,
+ [this.util.getVars().runId, this.util.getVars().attempt_number],
+ );
+ const specs: string[] =
+ dbRes.rows.length > 0 ? dbRes.rows.map((row) => row.name) : [];
+ return specs;
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ }
+
+ private async createAttempt() {
+ const client = await this.dbClient.connect();
+ try {
+ const runResponse = await client.query(
+ `INSERT INTO public."attempt" ("workflowId", "attempt", "repo", "committer", "type", "commitMsg", "branch")
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ ON CONFLICT ("workflowId", attempt) DO NOTHING
+ RETURNING id;`,
+ [
+ this.util.getVars().runId,
+ this.util.getVars().attempt_number,
+ this.util.getVars().repository,
+ this.util.getVars().committer,
+ this.util.getVars().tag,
+ this.util.getVars().commitMsg,
+ this.util.getVars().branch,
+ ],
+ );
+
+ if (runResponse.rows.length > 0) {
+ return runResponse.rows[0].id;
+ } else {
+ const res = await client.query(
+ `SELECT id FROM public."attempt" WHERE "workflowId" = $1 AND attempt = $2`,
+ [this.util.getVars().runId, this.util.getVars().attempt_number],
+ );
+ return res.rows[0].id;
+ }
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ }
+
+ private async createMatrix(attemptId: number) {
+ const client = await this.dbClient.connect();
+ try {
+ const matrixResponse = await client.query(
+ `INSERT INTO public."matrix" ("workflowId", "matrixId", "status", "attemptId")
+ VALUES ($1, $2, $3, $4)
+ ON CONFLICT ("matrixId", "attemptId") DO NOTHING
+ RETURNING id;`,
+ [
+ this.util.getVars().runId,
+ this.util.getVars().thisRunner,
+ "started",
+ attemptId,
+ ],
+ );
+ return matrixResponse.rows[0].id;
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ }
+
+ private async getFailedSpecsFromPreviousRun(
+ workflowId = Number(this.util.getVars().runId),
+ attempt_number = Number(this.util.getVars().attempt_number) - 1,
+ ) {
+ const client = await this.dbClient.connect();
+ try {
+ const dbRes = await client.query(
+ `SELECT name FROM public."specs"
+ WHERE "matrixId" IN
+ (SELECT id FROM public."matrix"
+ WHERE "attemptId" = (
+ SELECT id FROM public."attempt" WHERE "workflowId" = $1 and "attempt" = $2
+ )
+ ) AND status IN ('fail', 'queued', 'in-progress')`,
+ [workflowId, attempt_number],
+ );
+ const specs: string[] =
+ dbRes.rows.length > 0 ? dbRes.rows.map((row) => row.name) : [];
+ return specs;
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ }
+
+ private async addSpecsToMatrix(matrixId: number, specs: string[]) {
+ const client = await this.dbClient.connect();
+ try {
+ for (const spec of specs) {
+ const res = await client.query(
+ `INSERT INTO public."specs" ("name", "matrixId", "status") VALUES ($1, $2, $3) RETURNING id`,
+ [spec, matrixId, "queued"],
+ );
+ }
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ }
+
+ private async addLockGetTheSpecs(
+ attemptId: number,
+ specPattern: string | string[],
+ ignorePattern: string | string[],
+ ) {
+ const client = await this.dbClient.connect();
+ let specs: string[] = [];
+ let locked = false;
+ try {
+ while (!locked) {
+ const result = await client.query(
+ `UPDATE public."attempt" SET is_locked = true WHERE id = $1 AND is_locked = false RETURNING id`,
+ [attemptId],
+ );
+ if (result.rows.length === 1) {
+ locked = true;
+ let runningSpecs: string[] =
+ (await this.getAlreadyRunningSpecs()) ?? [];
+ if (typeof ignorePattern === "string") {
+ runningSpecs.push(ignorePattern);
+ ignorePattern = runningSpecs;
+ } else {
+ ignorePattern = runningSpecs.concat(ignorePattern);
+ }
+ specs = await this.getSpecsToRun(
+ specPattern,
+ ignorePattern,
+ attemptId,
+ );
+ return specs;
+ } else {
+ await this.sleep(5000);
+ }
+ }
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ }
+
+ private async updateTheSpecsAndReleaseLock(
+ attemptId: number,
+ specs: string[],
+ ) {
+ const client = await this.dbClient.connect();
+ try {
+ const matrixRes = await this.createMatrix(attemptId);
+ await this.addSpecsToMatrix(matrixRes, specs);
+ await client.query(
+ `UPDATE public."attempt" SET is_locked = false WHERE id = $1 AND is_locked = true RETURNING id`,
+ [attemptId],
+ );
+ } catch (err) {
+ console.log(err);
+ } finally {
+ client.release();
+ }
+ }
+
+ private sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms));
+ }
+
+ public async splitSpecs(
+ on: Cypress.PluginEvents,
+ config: Cypress.PluginConfigOptions,
+ ) {
+ try {
+ let specPattern = config.specPattern;
+ let ignorePattern: string | string[] = config.excludeSpecPattern;
+ const cypressSpecs = this.util.getVars().cypressSpecs;
+ const defaultSpec = "cypress/scripts/no_spec.ts";
+
+ if (cypressSpecs != "")
+ specPattern = cypressSpecs?.split(",").filter((val) => val !== "");
+ if (this.util.getVars().cypressRerun === "true") {
+ specPattern =
+ (await this.getFailedSpecsFromPreviousRun()) ?? defaultSpec;
+ }
+
+ const attempt = await this.createAttempt();
+ const specs =
+ (await this.addLockGetTheSpecs(attempt, specPattern, ignorePattern)) ??
+ [];
+ if (specs.length > 0 && !specs.includes(defaultSpec)) {
+ config.specPattern = specs.length == 1 ? specs[0] : specs;
+ await this.updateTheSpecsAndReleaseLock(attempt, specs);
+ } else {
+ config.specPattern = defaultSpec;
+ }
+
+ return config;
+ } catch (err) {
+ console.log(err);
+ } finally {
+ this.dbClient.end();
+ }
+ }
+}
diff --git a/app/client/cypress/scripts/util.ts b/app/client/cypress/scripts/util.ts
new file mode 100644
index 0000000000..858e09c01b
--- /dev/null
+++ b/app/client/cypress/scripts/util.ts
@@ -0,0 +1,149 @@
+import { Pool } from "pg";
+import AWS from "aws-sdk";
+import fs from "fs";
+import { Octokit } from "@octokit/rest";
+import fetch from "node-fetch";
+
+export interface DataItem {
+ name: string;
+ duration: string;
+}
+export default class util {
+ public getVars() {
+ return {
+ runId: this.getEnvValue("RUNID", { required: true }),
+ attempt_number: this.getEnvValue("ATTEMPT_NUMBER", { required: true }),
+ repository: this.getEnvValue("REPOSITORY", { required: true }),
+ committer: this.getEnvValue("COMMITTER", { required: true }),
+ tag: this.getEnvValue("TAG", { required: true }),
+ branch: this.getEnvValue("BRANCH", { required: true }),
+ cypressDbUser: this.getEnvValue("CYPRESS_DB_USER", { required: true }),
+ cypressDbHost: this.getEnvValue("CYPRESS_DB_HOST", { required: true }),
+ cypressDbName: this.getEnvValue("CYPRESS_DB_NAME", { required: true }),
+ cypressDbPwd: this.getEnvValue("CYPRESS_DB_PWD", { required: true }),
+ cypressS3Access: this.getEnvValue("CYPRESS_S3_ACCESS", {
+ required: true,
+ }),
+ cypressS3Secret: this.getEnvValue("CYPRESS_S3_SECRET", {
+ required: true,
+ }),
+ githubToken: process.env["GITHUB_TOKEN"],
+ commitMsg: this.getEnvValue("COMMIT_INFO_MESSAGE", { required: false }),
+ totalRunners: this.getEnvValue("TOTAL_RUNNERS", { required: false }),
+ thisRunner: this.getEnvValue("THIS_RUNNER", { required: true }),
+ cypressSpecs: this.getEnvValue("CYPRESS_SPECS", { required: false }),
+ cypressRerun: this.getEnvValue("CYPRESS_RERUN", { required: false }),
+ };
+ }
+
+ public async divideSpecsIntoBalancedGroups(
+ data: DataItem[],
+ numberOfGroups: number,
+ ): Promise {
+ const groups: DataItem[][] = Array.from(
+ { length: numberOfGroups },
+ () => [],
+ );
+ data.forEach((item) => {
+ // Find the group with the shortest total duration and add the item to it
+ const shortestGroupIndex = groups.reduce(
+ (minIndex, group, currentIndex) => {
+ const totalDuration = groups[minIndex].reduce(
+ (acc, item) => acc + Number(item.duration),
+ 0,
+ );
+ const totalDurationCurrent = group.reduce(
+ (acc, item) => acc + Number(item.duration),
+ 0,
+ );
+ return totalDurationCurrent < totalDuration ? currentIndex : minIndex;
+ },
+ 0,
+ );
+ groups[shortestGroupIndex].push(item);
+ });
+ return groups;
+ }
+
+ public getEnvValue(varName: string, { required = true }): string {
+ if (required && process.env[varName] === undefined) {
+ throw Error(
+ `${varName} is not defined.
+ Please check all the following environment variables are defined properly
+ [ RUNID, ATTEMPT_NUMBER, REPOSITORY, COMMITTER, TAG, BRANCH, THIS_RUNNER, CYPRESS_DB_USER, CYPRESS_DB_HOST, CYPRESS_DB_NAME, CYPRESS_DB_PWD, CYPRESS_S3_ACCESS, CYPRESS_S3_SECRET ].`,
+ );
+ }
+ return process.env[varName] ?? "";
+ }
+
+ //This is to setup the db client
+ public configureDbClient() {
+ const dbConfig = {
+ user: this.getVars().cypressDbUser,
+ host: this.getVars().cypressDbHost,
+ database: this.getVars().cypressDbName,
+ password: this.getVars().cypressDbPwd,
+ port: 5432,
+ connectionTimeoutMillis: 60000,
+ ssl: true,
+ keepalives: 30,
+ };
+ const dbClient = new Pool(dbConfig);
+ return dbClient;
+ }
+
+ // This is to setup the AWS client
+ public configureS3() {
+ AWS.config.update({ region: "ap-south-1" });
+ const s3client = new AWS.S3({
+ credentials: {
+ accessKeyId: this.getVars().cypressS3Access,
+ secretAccessKey: this.getVars().cypressS3Secret,
+ },
+ });
+ return s3client;
+ }
+
+ // This is to upload files to s3 when required
+ public async uploadToS3(s3Client: AWS.S3, filePath: string, key: string) {
+ const fileContent = fs.readFileSync(filePath);
+ const params = {
+ Bucket: "appsmith-internal-cy-db",
+ Key: key,
+ Body: fileContent,
+ };
+ return await s3Client.upload(params).promise();
+ }
+
+ public async getActiveRunners() {
+ const octokit = new Octokit({
+ auth: this.getVars().githubToken,
+ request: {
+ fetch: fetch,
+ },
+ });
+ try {
+ const repo: string[] = this.getVars().repository.split("/");
+ const response = await octokit.request(
+ "GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs",
+ {
+ owner: repo[0],
+ repo: repo[1],
+ run_id: Number(this.getVars().runId),
+ per_page: 100,
+ headers: {
+ "X-GitHub-Api-Version": "2022-11-28",
+ },
+ },
+ );
+ const active_runners = response.data.jobs.filter(
+ (job) =>
+ (job.status === "in_progress" || job.status === "queued") &&
+ job.run_attempt === Number(this.getVars().attempt_number),
+ );
+ return active_runners.length;
+ } catch (error) {
+ console.error("Error:", error);
+ }
+ }
+}
diff --git a/app/client/cypress_ci_custom.config.ts b/app/client/cypress_ci_custom.config.ts
index c530a480b9..feadeff56e 100644
--- a/app/client/cypress_ci_custom.config.ts
+++ b/app/client/cypress_ci_custom.config.ts
@@ -6,7 +6,7 @@ export default defineConfig({
requestTimeout: 60000,
responseTimeout: 60000,
pageLoadTimeout: 60000,
- videoUploadOnPasses: false,
+ video: true,
numTestsKeptInMemory: 5,
experimentalMemoryManagement: true,
reporter: "cypress-mochawesome-reporter",
diff --git a/app/client/cypress_ci_hosted.config.ts b/app/client/cypress_ci_hosted.config.ts
index 6b40587510..33948c1aa5 100644
--- a/app/client/cypress_ci_hosted.config.ts
+++ b/app/client/cypress_ci_hosted.config.ts
@@ -7,7 +7,7 @@ export default defineConfig({
responseTimeout: 60000,
pageLoadTimeout: 60000,
videoCompression: false,
- videoUploadOnPasses: false,
+ video: true,
numTestsKeptInMemory: 5,
experimentalMemoryManagement: true,
reporter: "cypress-mochawesome-reporter",
diff --git a/app/client/package.json b/app/client/package.json
index 92d1eb0593..ca4099a006 100644
--- a/app/client/package.json
+++ b/app/client/package.json
@@ -140,7 +140,6 @@
"normalizr": "^3.3.0",
"object-hash": "^3.0.0",
"path-to-regexp": "^6.2.0",
- "pg": "^8.11.3",
"popper.js": "^1.15.0",
"prismjs": "^1.27.0",
"proxy-memoize": "^1.2.0",
@@ -231,6 +230,7 @@
"@babel/helper-string-parser": "^7.19.4",
"@craco/craco": "^7.0.0",
"@faker-js/faker": "^7.4.0",
+ "@octokit/rest": "^20.0.1",
"@peculiar/webcrypto": "^1.4.3",
"@redux-saga/testing-utils": "^1.1.5",
"@sentry/webpack-plugin": "^1.18.9",
@@ -252,6 +252,7 @@
"@types/node": "^10.12.18",
"@types/node-forge": "^0.10.0",
"@types/object-hash": "^2.2.1",
+ "@types/pg": "^8.10.2",
"@types/prismjs": "^1.16.1",
"@types/react": "^17.0.2",
"@types/react-beautiful-dnd": "^11.0.4",
@@ -288,7 +289,7 @@
"compression-webpack-plugin": "^10.0.0",
"cra-bundle-analyzer": "^0.1.0",
"cy-verify-downloads": "^0.0.5",
- "cypress": "^12.17.4",
+ "cypress": "13.2.0",
"cypress-file-upload": "^4.1.1",
"cypress-image-snapshot": "^4.0.1",
"cypress-mochawesome-reporter": "^3.5.1",
@@ -322,6 +323,7 @@
"json5": "^2.2.3",
"lint-staged": "^13.2.0",
"msw": "^0.28.0",
+ "pg": "^8.11.3",
"plop": "^3.1.1",
"postcss": "^8.4.30",
"postcss-at-rules-variables": "^0.3.0",
diff --git a/app/client/yarn.lock b/app/client/yarn.lock
index 8a5e4e5c62..7520c1d935 100644
--- a/app/client/yarn.lock
+++ b/app/client/yarn.lock
@@ -2265,9 +2265,9 @@ __metadata:
languageName: node
linkType: hard
-"@cypress/request@npm:2.88.12":
- version: 2.88.12
- resolution: "@cypress/request@npm:2.88.12"
+"@cypress/request@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "@cypress/request@npm:3.0.0"
dependencies:
aws-sign2: ~0.7.0
aws4: ^1.8.0
@@ -2287,7 +2287,7 @@ __metadata:
tough-cookie: ^4.1.3
tunnel-agent: ^0.6.0
uuid: ^8.3.2
- checksum: 2c6fbf7f3127d41bffca8374beaa8cf95450495a8a077b00309ea9d94dd2a4da450a77fe038e8ad26c97cdd7c39b65c53c850f8338ce9bc2dbe23ce2e2b48329
+ checksum: 8ec81075b800b84df8a616dce820a194d562a35df251da234f849344022979f3675baa0b82988843f979a488e39bc1eec6204cfe660c75ace9bf4d2951edec43
languageName: node
linkType: hard
@@ -3941,6 +3941,133 @@ __metadata:
languageName: node
linkType: hard
+"@octokit/auth-token@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "@octokit/auth-token@npm:4.0.0"
+ checksum: d78f4dc48b214d374aeb39caec4fdbf5c1e4fd8b9fcb18f630b1fe2cbd5a880fca05445f32b4561f41262cb551746aeb0b49e89c95c6dd99299706684d0cae2f
+ languageName: node
+ linkType: hard
+
+"@octokit/core@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "@octokit/core@npm:5.0.0"
+ dependencies:
+ "@octokit/auth-token": ^4.0.0
+ "@octokit/graphql": ^7.0.0
+ "@octokit/request": ^8.0.2
+ "@octokit/request-error": ^5.0.0
+ "@octokit/types": ^11.0.0
+ before-after-hook: ^2.2.0
+ universal-user-agent: ^6.0.0
+ checksum: 1a5d1112a2403d146aa1db7aaf81a31192ef6b0310a1e6f68c3e439fded22bd4b3a930f5071585e6ca0f2f5e7fc4a1aac68910525b71b03732c140e362d26a33
+ languageName: node
+ linkType: hard
+
+"@octokit/endpoint@npm:^9.0.0":
+ version: 9.0.0
+ resolution: "@octokit/endpoint@npm:9.0.0"
+ dependencies:
+ "@octokit/types": ^11.0.0
+ is-plain-object: ^5.0.0
+ universal-user-agent: ^6.0.0
+ checksum: 0e402c4d0fbe5b8053630cedb30dde5074bb6410828a05dc93d7e0fdd6c17f9a44b66586ef1a4e4ee0baa8d34ef7d6f535e2f04d9ea42909b7fc7ff55ce56a48
+ languageName: node
+ linkType: hard
+
+"@octokit/graphql@npm:^7.0.0":
+ version: 7.0.1
+ resolution: "@octokit/graphql@npm:7.0.1"
+ dependencies:
+ "@octokit/request": ^8.0.1
+ "@octokit/types": ^11.0.0
+ universal-user-agent: ^6.0.0
+ checksum: 7ee907987b1b8312c6f870c44455cbd3eed805bb1a4095038f4e7e62ee2e006bd766f2a71dfbe56b870cd8f7558309c602f00d3e252fe59578f4acf6249a4f17
+ languageName: node
+ linkType: hard
+
+"@octokit/openapi-types@npm:^18.0.0":
+ version: 18.0.0
+ resolution: "@octokit/openapi-types@npm:18.0.0"
+ checksum: d487d6c6c1965e583eee417d567e4fe3357a98953fc49bce1a88487e7908e9b5dbb3e98f60dfa340e23b1792725fbc006295aea071c5667a813b9c098185b56f
+ languageName: node
+ linkType: hard
+
+"@octokit/plugin-paginate-rest@npm:^8.0.0":
+ version: 8.0.0
+ resolution: "@octokit/plugin-paginate-rest@npm:8.0.0"
+ dependencies:
+ "@octokit/types": ^11.0.0
+ peerDependencies:
+ "@octokit/core": ">=5"
+ checksum: b5d7cee50523862c6ce7be057f7200e14ee4dcded462f27304c822c960a37efa23ed51080ea879f5d1e56e78f74baa17d2ce32eed5d726794abc35755777e32c
+ languageName: node
+ linkType: hard
+
+"@octokit/plugin-request-log@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "@octokit/plugin-request-log@npm:4.0.0"
+ peerDependencies:
+ "@octokit/core": ">=5"
+ checksum: 2a8a6619640942092009a9248ceeb163ce01c978e2d7b2a7eb8686bd09a04b783c4cd9071eebb16652d233587abcde449a02ce4feabc652f0a171615fb3e9946
+ languageName: node
+ linkType: hard
+
+"@octokit/plugin-rest-endpoint-methods@npm:^9.0.0":
+ version: 9.0.0
+ resolution: "@octokit/plugin-rest-endpoint-methods@npm:9.0.0"
+ dependencies:
+ "@octokit/types": ^11.0.0
+ peerDependencies:
+ "@octokit/core": ">=5"
+ checksum: 8795cb29be042c839098886a03c2ec6051e3fd7a29f16f4f8a487aa2d85ceb00df8a4432499a43af550369bd730ce9b1b9d7eeff768745b80a3e67698ca9a5dd
+ languageName: node
+ linkType: hard
+
+"@octokit/request-error@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "@octokit/request-error@npm:5.0.0"
+ dependencies:
+ "@octokit/types": ^11.0.0
+ deprecation: ^2.0.0
+ once: ^1.4.0
+ checksum: 2012eca66f6b8fa4038b3bfe81d65a7134ec58e2caf45d229aca13b9653ab260abd95229bd1a8c11180ee0bcf738e2556831a85de28f39b175175653c3b79fdd
+ languageName: node
+ linkType: hard
+
+"@octokit/request@npm:^8.0.1, @octokit/request@npm:^8.0.2":
+ version: 8.1.1
+ resolution: "@octokit/request@npm:8.1.1"
+ dependencies:
+ "@octokit/endpoint": ^9.0.0
+ "@octokit/request-error": ^5.0.0
+ "@octokit/types": ^11.1.0
+ is-plain-object: ^5.0.0
+ universal-user-agent: ^6.0.0
+ checksum: dec3ba2cba14739159cd8d1653ad8ac6d58095e4ac294d312d20ce2c63c60c3cad2e5499137244dba3d681fd5cd7f74b4b5d4df024a19c0ee1831204e5a3a894
+ languageName: node
+ linkType: hard
+
+"@octokit/rest@npm:^20.0.1":
+ version: 20.0.1
+ resolution: "@octokit/rest@npm:20.0.1"
+ dependencies:
+ "@octokit/core": ^5.0.0
+ "@octokit/plugin-paginate-rest": ^8.0.0
+ "@octokit/plugin-request-log": ^4.0.0
+ "@octokit/plugin-rest-endpoint-methods": ^9.0.0
+ checksum: 9fb2e154a498e00598379b09d76cc7b67b3801e9c97d753f1a76e1163924188bf4cb1411ec152a038ae91e97b86d7146ff220b05adfb6e500e2300c87e14100a
+ languageName: node
+ linkType: hard
+
+"@octokit/types@npm:^11.0.0, @octokit/types@npm:^11.1.0":
+ version: 11.1.0
+ resolution: "@octokit/types@npm:11.1.0"
+ dependencies:
+ "@octokit/openapi-types": ^18.0.0
+ checksum: 72627a94ddaf7bc14db06572bcde67649aad608cd86548818380db9305f4c0ca9ca078a62dd883858a267e8ec8fd596a0fce416aa04197c439b9548efef609a7
+ languageName: node
+ linkType: hard
+
"@open-draft/until@npm:^1.0.3":
version: 1.0.3
resolution: "@open-draft/until@npm:1.0.3"
@@ -8076,10 +8203,10 @@ __metadata:
languageName: node
linkType: hard
-"@types/node@npm:*, @types/node@npm:>=10.0.0":
- version: 18.14.5
- resolution: "@types/node@npm:18.14.5"
- checksum: 415fb0edc132baa9580f1b7a381a3f10b662f5d7a7d11641917fa0961788ccede3272badc414aadc47306e9fc35c5f6c59159ac470b46d3f3a15fb0446224c8c
+"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^18.17.5":
+ version: 18.17.15
+ resolution: "@types/node@npm:18.17.15"
+ checksum: eed11d4398ccdb999a4c65658ee75de621a4ad57aece48ed2fb8803b1e2711fadf58d8aefbdb0a447d69cf3cba602ca32fe0fc92077575950a796e1dc13baa0f
languageName: node
linkType: hard
@@ -8090,7 +8217,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/node@npm:^16.0.0, @types/node@npm:^16.18.39":
+"@types/node@npm:^16.0.0":
version: 16.18.40
resolution: "@types/node@npm:16.18.40"
checksum: a683930491b4fd7cb2dc7684e32bbeedc4a83fb1949a7b15ea724fbfaa9988cec59091f169a3f1090cb91992caba8c1a7d50315b2c67c6e2579a3788bb09eec4
@@ -8118,6 +8245,17 @@ __metadata:
languageName: node
linkType: hard
+"@types/pg@npm:^8.10.2":
+ version: 8.10.2
+ resolution: "@types/pg@npm:8.10.2"
+ dependencies:
+ "@types/node": "*"
+ pg-protocol: "*"
+ pg-types: ^4.0.1
+ checksum: 49da89f64cec1bd12a3fbc0c72b17d685c2fee579726a529f62fcab395dbc5696d80455073409947a577164b3c53a90181a331e4a5d9357679f724d4ce37f4b9
+ languageName: node
+ linkType: hard
+
"@types/prettier@npm:^2.1.5":
version: 2.7.0
resolution: "@types/prettier@npm:2.7.0"
@@ -9913,6 +10051,7 @@ __metadata:
"@loadable/component": ^5.15.3
"@manaflair/redux-batch": ^1.0.0
"@mantine/hooks": ^5.10.1
+ "@octokit/rest": ^20.0.1
"@peculiar/webcrypto": ^1.4.3
"@redux-saga/testing-utils": ^1.1.5
"@sentry/react": ^6.2.4
@@ -9941,6 +10080,7 @@ __metadata:
"@types/node": ^10.12.18
"@types/node-forge": ^0.10.0
"@types/object-hash": ^2.2.1
+ "@types/pg": ^8.10.2
"@types/prismjs": ^1.16.1
"@types/react": ^17.0.2
"@types/react-beautiful-dnd": ^11.0.4
@@ -10003,7 +10143,7 @@ __metadata:
craco-babel-loader: ^1.0.4
cssnano: ^6.0.1
cy-verify-downloads: ^0.0.5
- cypress: ^12.17.4
+ cypress: 13.2.0
cypress-file-upload: ^4.1.1
cypress-image-snapshot: ^4.0.1
cypress-log-to-output: ^1.1.2
@@ -11081,6 +11221,13 @@ __metadata:
languageName: node
linkType: hard
+"before-after-hook@npm:^2.2.0":
+ version: 2.2.3
+ resolution: "before-after-hook@npm:2.2.3"
+ checksum: a1a2430976d9bdab4cd89cb50d27fa86b19e2b41812bf1315923b0cba03371ebca99449809226425dd3bcef20e010db61abdaff549278e111d6480034bebae87
+ languageName: node
+ linkType: hard
+
"better-opn@npm:^3.0.2":
version: 3.0.2
resolution: "better-opn@npm:3.0.2"
@@ -13583,13 +13730,13 @@ __metadata:
languageName: node
linkType: hard
-"cypress@npm:^12.17.4":
- version: 12.17.4
- resolution: "cypress@npm:12.17.4"
+"cypress@npm:13.2.0":
+ version: 13.2.0
+ resolution: "cypress@npm:13.2.0"
dependencies:
- "@cypress/request": 2.88.12
+ "@cypress/request": ^3.0.0
"@cypress/xvfb": ^1.2.4
- "@types/node": ^16.18.39
+ "@types/node": ^18.17.5
"@types/sinonjs__fake-timers": 8.1.1
"@types/sizzle": ^2.3.2
arch: ^2.2.0
@@ -13632,7 +13779,7 @@ __metadata:
yauzl: ^2.10.0
bin:
cypress: bin/cypress
- checksum: c9c79f5493b23e9c8cfb92d45d50ea9d0fae54210dde203bfa794a79436faf60108d826fe9007a7d67fddf7919802ad8f006b7ae56c5c198c75d5bc85bbc851b
+ checksum: 7647814f07626bd63e7b8dc4d066179fa40bf492c588bbc2626d983a2baab6cb77c29958dc92442f277e0a8e94866decc51c4de306021739c47e32baf5970219
languageName: node
linkType: hard
@@ -13945,6 +14092,13 @@ __metadata:
languageName: node
linkType: hard
+"deprecation@npm:^2.0.0":
+ version: 2.3.1
+ resolution: "deprecation@npm:2.3.1"
+ checksum: f56a05e182c2c195071385455956b0c4106fe14e36245b00c689ceef8e8ab639235176a96977ba7c74afb173317fac2e0ec6ec7a1c6d1e6eaa401c586c714132
+ languageName: node
+ linkType: hard
+
"deps-sort@npm:^2.0.0, deps-sort@npm:^2.0.1":
version: 2.0.1
resolution: "deps-sort@npm:2.0.1"
@@ -22271,7 +22425,7 @@ __metadata:
languageName: node
linkType: hard
-"obuf@npm:^1.0.0, obuf@npm:^1.1.2":
+"obuf@npm:^1.0.0, obuf@npm:^1.1.2, obuf@npm:~1.1.2":
version: 1.1.2
resolution: "obuf@npm:1.1.2"
checksum: 41a2ba310e7b6f6c3b905af82c275bf8854896e2e4c5752966d64cbcd2f599cfffd5932006bcf3b8b419dfdacebb3a3912d5d94e10f1d0acab59876c8757f27f
@@ -22910,6 +23064,13 @@ __metadata:
languageName: node
linkType: hard
+"pg-numeric@npm:1.0.2":
+ version: 1.0.2
+ resolution: "pg-numeric@npm:1.0.2"
+ checksum: 8899f8200caa1744439a8778a9eb3ceefb599d893e40a09eef84ee0d4c151319fd416634a6c0fc7b7db4ac268710042da5be700b80ef0de716fe089b8652c84f
+ languageName: node
+ linkType: hard
+
"pg-pool@npm:^3.6.1":
version: 3.6.1
resolution: "pg-pool@npm:3.6.1"
@@ -22919,7 +23080,7 @@ __metadata:
languageName: node
linkType: hard
-"pg-protocol@npm:^1.6.0":
+"pg-protocol@npm:*, pg-protocol@npm:^1.6.0":
version: 1.6.0
resolution: "pg-protocol@npm:1.6.0"
checksum: e12662d2de2011e0c3a03f6a09f435beb1025acdc860f181f18a600a5495dc38a69d753bbde1ace279c8c442536af9c1a7c11e1d0fe3fad3aa1348b28d9d2683
@@ -22939,6 +23100,21 @@ __metadata:
languageName: node
linkType: hard
+"pg-types@npm:^4.0.1":
+ version: 4.0.1
+ resolution: "pg-types@npm:4.0.1"
+ dependencies:
+ pg-int8: 1.0.1
+ pg-numeric: 1.0.2
+ postgres-array: ~3.0.1
+ postgres-bytea: ~3.0.0
+ postgres-date: ~2.0.1
+ postgres-interval: ^3.0.0
+ postgres-range: ^1.1.1
+ checksum: 05258ef2f27a75f1bf4e243f36bb749f85148339d3be818147bcc4aebe019ad7589a6869150713140250d81e5a46ec25dc6e0a031ea77e23db5ca232a0d7a3dc
+ languageName: node
+ linkType: hard
+
"pg@npm:^8.11.3":
version: 8.11.3
resolution: "pg@npm:8.11.3"
@@ -24440,6 +24616,13 @@ __metadata:
languageName: node
linkType: hard
+"postgres-array@npm:~3.0.1":
+ version: 3.0.2
+ resolution: "postgres-array@npm:3.0.2"
+ checksum: 5955f9dffeb6fa960c1a0b04fd4b2ba16813ddb636934ad26f902e4d76a91c0b743dcc6edc4cffc52deba7d547505e0020adea027c1d50a774f989cf955420d1
+ languageName: node
+ linkType: hard
+
"postgres-bytea@npm:~1.0.0":
version: 1.0.0
resolution: "postgres-bytea@npm:1.0.0"
@@ -24447,6 +24630,15 @@ __metadata:
languageName: node
linkType: hard
+"postgres-bytea@npm:~3.0.0":
+ version: 3.0.0
+ resolution: "postgres-bytea@npm:3.0.0"
+ dependencies:
+ obuf: ~1.1.2
+ checksum: 5f917a003fcaa0df7f285e1c37108ad474ce91193466b9bd4bcaecef2cdea98ca069c00aa6a8dbe6d2e7192336cadc3c9b36ae48d1555a299521918e00e2936b
+ languageName: node
+ linkType: hard
+
"postgres-date@npm:~1.0.4":
version: 1.0.7
resolution: "postgres-date@npm:1.0.7"
@@ -24454,6 +24646,13 @@ __metadata:
languageName: node
linkType: hard
+"postgres-date@npm:~2.0.1":
+ version: 2.0.1
+ resolution: "postgres-date@npm:2.0.1"
+ checksum: 0304bf8641a01412e4f5c3a374604e2e3dbc9dbee71d30df12fe60b32560c5674f887c2d15bafa2996f3b618b617398e7605f0e3669db43f31e614dfe69f8de7
+ languageName: node
+ linkType: hard
+
"postgres-interval@npm:^1.1.0":
version: 1.2.0
resolution: "postgres-interval@npm:1.2.0"
@@ -24463,6 +24662,20 @@ __metadata:
languageName: node
linkType: hard
+"postgres-interval@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "postgres-interval@npm:3.0.0"
+ checksum: c7a1cf006de97de663b6b8c4d2b167aa9909a238c4866a94b15d303762f5ac884ff4796cd6e2111b7f0a91302b83c570453aa8506fd005b5a5d5dfa87441bebc
+ languageName: node
+ linkType: hard
+
+"postgres-range@npm:^1.1.1":
+ version: 1.1.3
+ resolution: "postgres-range@npm:1.1.3"
+ checksum: bf7e194a18c490d02bda0bd02035a8da454d8fd2b22c55d3d03f185c038b2a6f52d0804417d8090864afefc2b7ed664b2d12c2454a4a0f545dcbbb86488fbdf1
+ languageName: node
+ linkType: hard
+
"postinstall-postinstall@npm:^2.1.0":
version: 2.1.0
resolution: "postinstall-postinstall@npm:2.1.0"
@@ -30031,6 +30244,13 @@ __metadata:
languageName: node
linkType: hard
+"universal-user-agent@npm:^6.0.0":
+ version: 6.0.0
+ resolution: "universal-user-agent@npm:6.0.0"
+ checksum: 5092bbc80dd0d583cef0b62c17df0043193b74f425112ea6c1f69bc5eda21eeec7a08d8c4f793a277eb2202ffe9b44bec852fa3faff971234cd209874d1b79ef
+ languageName: node
+ linkType: hard
+
"universalify@npm:^0.1.0":
version: 0.1.2
resolution: "universalify@npm:0.1.2"