One way to have Cypress automation run is through a series of GitHub Actions workflows. The one I’m going to show you runs through a defined matrix and runs test in parallel.

Take for example this Matrix. Think of this as an array of test you want running in your suite. Note that we can filter this array in our workflows to our advantage. Also note that all the properties you define here will be able to be passed to each matrix job in our workflows as well.

{
  "include": [
    {
      "name": "vtr store",
      "app": "misc",
      "group": "compare",
      "tag": "COMPARE vtr store",
      "country": "USA",
      "countryHeader": "us",
      "filePath": "cypress/e2e/misc/compare/vtr.store.spec.ts",
      "storeId": "1",
      "backupStoreId": "20001"
    },
    {
      "name": "create deal",
      "app": "sales",
      "group": "desking",
      "tag": "DESKING create deal",
      "country": "USA",
      "countryHeader": "us",
      "filePath": "cypress/e2e/sales/desking/create.deal.spec.ts",
      "storeId": "2",
      "backupStoreId": "20002"
    }
   ]
}

I also created this quick iife to generate a matrix like above quickly

const fs = require('fs');
const path = require('path');
let filePaths = [];

function recFindByExt(startPath, filter) {
  if (!fs.existsSync(startPath)) {
    console.log('no dir ', startPath);
    return;
  }
  let files = fs.readdirSync(startPath);
  for (var i = 0; i < files.length; i++) {
    let filename = path.join(startPath, files[i]);
    let stat = fs.lstatSync(filename);
    if (stat.isDirectory()) {
      recFindByExt(filename, filter); //recurse
    } else if (filename.indexOf(filter) >= 0) {
      console.log('-- found: ', filename);
      filePaths.push(filename);
    }
  }
}

(async () => {
  try {
    let includes = [];
    let specpath = path.join(__dirname, '../cypress/e2e');
    recFindByExt(specpath, '.spec.ts');
    const canadaSpecFiles = filePaths.filter((filePath) =>
      filePath.includes('canada')
    );
    const usaSpecFiles = filePaths.filter(
      (filePath) => !filePath.includes('canada')
    );
    usaSpecFiles.map((filepath, index) => {
      const splitPath = filepath.split('/');
      const name = splitPath[splitPath.length - 1]
        .replace('.spec.ts', '')
        .replace(/\./g, ' ');
      const integrationIndex = splitPath.indexOf('e2e');
      const group = splitPath[integrationIndex + 2].replace(/[\W_]+/g, '-');
      const appName = splitPath[integrationIndex + 1].replace(/[\W_]+/g, '-');
      includes.push({
        name: name,
        app: appName,
        group: group,
        tag: `${group.toUpperCase()} ${name}`,
        country: 'USA',
        countryHeader: 'us',
        filePath: filepath.substring(filepath.indexOf('cypress')),
        storeId: String(index + 1),
        backupStoreId: String(index + 20001),
      });
    });
    canadaSpecFiles.map((filepath, index) => {
      const splitPath = filepath.split('/');
      const name = splitPath[splitPath.length - 1]
        .replace('.spec.ts', '')
        .replace(/\./g, ' ');
      const integrationIndex = splitPath.indexOf('e2e');
      const group = splitPath[integrationIndex + 2].replace(/[\W_]+/g, '-');
      const appName = splitPath[integrationIndex + 1].replace(/[\W_]+/g, '-');
      includes.push({
        name: name,
        group: group,
        app: appName,
        tag: `${group.toUpperCase()} ${name}`,
        country: 'CAN',
        countryHeader: 'ca',
        filePath: filepath.substring(filepath.indexOf('cypress')),
        storeId: String(index + 10000),
        backupStoreId: String(index + 25001),
      });
    });
    const allIncludes = {
      include: includes,
    };
    const writePath = path.join(__dirname, `matrix.json`);
    fs.writeFileSync(writePath, JSON.stringify(allIncludes));
  } catch (error) {
    console.error(error);
  }
})();

Defining the GitHub Actions workflows and filtering your include matrix as seen above.

name: Misc Cypress Suite
on:
  # schedule:
  # - cron: '0 17-22/4 * * 1-5' # 7am -> 3pm
  # - cron: '59 23 * * 1-5' # 5pm
  workflow_dispatch:
env:
  CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
  # Recommended: pass the GitHub token lets this action correctly
  # determine the unique run id necessary to re-run the checks
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  CYPRESS_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}
  HOST_BASE: ${{ secrets.HOST_BASE }}
  # https://github.com/cypress-io/cypress/issues/2777
  BASE_URL: ${{ secrets.BASE_URL }}
  CYPRESS_ADMIN_PASSWORD: ${{ secrets.CYPRESS_ADMIN_PASSWORD }}
  CYPRESS_ADMIN_PORTAL_URL: ${{ secrets.CYPRESS_ADMIN_PORTAL_URL }}
  CYPRESS_ADMIN_USERNAME: ${{ secrets.CYPRESS_ADMIN_USERNAME }}
  CYPRESS_CORE_API_URL: ${{ secrets.CYPRESS_CORE_API_URL }}
  CYPRESS_COUNTRY_HEADER: 'us'
  CYPRESS_COUNTRY_SHORT_CODE: 'USA'
  CYPRESS_HOST_BASE: ${{ secrets.CYPRESS_HOST_BASE }}
  CYPRESS_INVENTORY_API_URL: ${{ secrets.CYPRESS_INVENTORY_API_URL }}
  CYPRESS_VEHICLE_API_URL: ${{ secrets.CYPRESS_VEHICLE_API_URL }}
  CYPRESS_SALES_PORTAL_URL: ${{ secrets.CYPRESS_SALES_PORTAL_URL }}
  CYPRESS_TEST_ENV: 'staging'
  CYPRESS_PGHOST_STAGING: ${{ secrets.CYPRESS_PGHOST_STAGING }}
  CYPRESS_PGPORT_STAGING: ${{ secrets.CYPRESS_PGPORT_STAGING }}
  CYPRESS_PGDATABASE_CORE_STAGING: ${{ secrets.CYPRESS_PGDATABASE_CORE_STAGING }}
  CYPRESS_PGDATABASE_LEADS_STAGING: ${{ secrets.CYPRESS_PGDATABASE_LEADS_STAGING }}
  CYPRESS_PGDATABASE_USERS_STAGING: ${{ secrets.CYPRESS_PGDATABASE_USERS_STAGING }}
  CYPRESS_PGDATABASE_NOTIFICATIONS_STAGING: ${{ secrets.CYPRESS_PGDATABASE_NOTIFICATIONS_STAGING }}
  CYPRESS_PGPASSWORD_STAGING: ${{ secrets.CYPRESS_PGPASSWORD_STAGING }}
  CYPRESS_PGUSER_STAGING: ${{ secrets.CYPRESS_PGUSER_STAGING }}
  CYPRESS_TWILIO_ACCOUNTSID: ${{ secrets.CYPRESS_TWILIO_ACCOUNTSID }}
  CYPRESS_TWILIO_AUTH_HEADER: ${{ secrets.CYPRESS_TWILIO_AUTH_HEADER }}
  CYPRESS_TWILIO_AUTHTOKEN: ${{ secrets.CYPRESS_TWILIO_AUTHTOKEN }}
  CYPRESS_TWILIO_PHONE_ONE: ${{ secrets.CYPRESS_TWILIO_PHONE_ONE }}
  CYPRESS_TWILIO_PHONE_TWO: ${{ secrets.CYPRESS_TWILIO_PHONE_TWO }}
  CYPRESS_TWILIO_PHONE_THREE: ${{ secrets.CYPRESS_TWILIO_PHONE_THREE }}
  CYPRESS_USERS_API_URL: ${{ secrets.CYPRESS_USERS_API_URL }}
  CYPRESS_DT_DEALER_ID: ${{ secrets.CYPRESS_DT_DEALER_ID }}
  CYPRESS_DT_LENDER_ID: ${{ secrets.CYPRESS_DT_LENDER_ID }}
  CYPRESS_R1_SENDER_NAME_CODE: ${{ secrets.CYPRESS_R1_SENDER_NAME_CODE }}
  CYPRESS_R1_SENDER_ID: ${{ secrets.CYPRESS_R1_SENDER_ID }}
  CYPRESS_R1_TARGET_ID: ${{ secrets.CYPRESS_R1_TARGET_ID }}
  CYPRESS_R1_MESSAGE_TYPE: ${{ secrets.CYPRESS_R1_MESSAGE_TYPE }}
  CYPRESS_R1_DEALER_ID: ${{ secrets.CYPRESS_R1_DEALER_ID }}
  CYPRESS_CUDL_DEALER_ID: ${{ secrets.CYPRESS_CUDL_DEALER_ID }}
  CYPRESS_CUDL_LENDER_ID: ${{ secrets.CYPRESS_CUDL_LENDER_ID }}
  CYPRESS_R1_LENDER_ID: ${{ secrets.CYPRESS_R1_LENDER_ID }}
  CYPRESS_R1_SIMULATION_MESSAGE_TYPE: ${{ secrets.CYPRESS_R1_SIMULATION_MESSAGE_TYPE }}
  CYPRESS_R1_TARGET_ID_SIMULATION: ${{ secrets.CYPRESS_R1_TARGET_ID_SIMULATION }}
  CYPRESS_R1_SENDER_ID_SIMULATION: ${{ secrets.CYPRESS_R1_SENDER_ID_SIMULATION }}
  CYPRESS_R1_WHITE_LIST: ${{ secrets.CYPRESS_R1_WHITE_LIST }}
  CYPRESS_WEB_PASS: ${{ secrets.CYPRESS_WEB_PASS }}
  CYPRESS_WEB_USER: ${{ secrets.CYPRESS_WEB_USER }}
  CYPRESS_WEB_CLIENT_STORE_EMAIL_USER: ${{ secrets.WEB_CLIENT_STORE_EMAIL_USER }}
  CYPRESS_WEB_CLIENT_STORE_EMAIL_PASS: ${{ secrets.WEB_CLIENT_STORE_EMAIL_PASS }}
  CYPRESS_WEB_CLIENT_LEADS_EMAIL_USER: ${{ secrets.WEB_CLIENT_LEADS_EMAIL_USER }}
  CYPRESS_WEB_CLIENT_LEADS_EMAIL_PASS: ${{ secrets.WEB_CLIENT_LEADS_EMAIL_PASS }}
  CYPRESS_VTR_STORE_EMAIL_PASS: ${{ secrets.VTR_STORE_EMAIL_PASS }}
  CYPRESS_VTR_STORE_EMAIL_USER: ${{ secrets.VTR_STORE_EMAIL_USER }}
  CYPRESS_VTR_LEAD_EMAIL_PASS: ${{ secrets.VTR_LEAD_EMAIL_PASS }}
  CYPRESS_VTR_LEAD_EMAIL_USER: ${{ secrets.VTR_LEAD_EMAIL_USER }}
  CYPRESS_VTR_CAN_EMAIL_USER: ${{ secrets.VTR_CAN_EMAIL_USER }}
  CYPRESS_VTR_CAN_EMAIL_PASS: ${{ secrets.VTR_CAN_EMAIL_PASS }}
  CYPRESS_IMAP_EMAIL_HOST: ${{ secrets.IMAP_EMAIL_HOST }}
  CYPRESS_IMAP_EMAIL_PORT: ${{ secrets.IMAP_EMAIL_PORT }}
  INVENTORY_IMPORT_HOST: ${{ secrets.INVENTORY_IMPORT_HOST }}
  INVENTORY_IMPORT_PASS: ${{ secrets.INVENTORY_IMPORT_PASS }}
  INVENTORY_IMPORT_USER: ${{ secrets.INVENTORY_IMPORT_USER }}
  CYPRESS_DB_ENCRYPTION_KEY: ${{ secrets.DB_ENCRYPTION_KEY }}
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
  SLACK_TEST_AUTOMATION_CHANNEL_ID: ${{ secrets.SLACK_TEST_AUTOMATION_CHANNEL_ID }}
  SLACK_TRUECAR_BOT_TOKEN: ${{ secrets.SLACK_TRUECAR_BOT_TOKEN }}
  CYPRESS_TEST_PHONE: ${{ secrets.CYPRESS_TEST_PHONE }}
  CYPRESS_TEST_CODE: ${{ secrets.CYPRESS_TEST_CODE }}
  STAGING_USA_MONGO_URI: ${{ secrets.STAGING_USA_MONGO_URI }}
  STAGING_CAN_MONGO_URI: ${{ secrets.STAGING_CAN_MONGO_URI }}
jobs:
  install:
    name: Install
    runs-on: ubuntu-latest
    timeout-minutes: 80
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Install
        uses: cypress-io/github-action@v4
        with:
          runTests: false
  set_misc_matrix:
    name: Set Misc Matrix
    needs: install
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set Matrix
        id: set-matrix
        run: |
          SPECS="`jq -r --arg SPECS "$SPECS" '.include |= map(select([.group] | inside(["misc"])))' workflow-matrix/matrix.json`"
          echo "matrix="$SPECS"" >> $GITHUB_OUTPUT
  misc-suite:
    name: Misc
    needs: set_misc_matrix
    runs-on: ubuntu-latest
    container:
      image: cypress/browsers:node16.16.0-chrome105-ff104-edge
      options: --user 1001 # ← THIS IS THE IMPORTANT LINE!
    strategy:
      matrix: ${{fromJson(needs.set_misc_matrix.outputs.matrix)}}
      fail-fast: false
    timeout-minutes: 50
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Report
        run: yarn clean:reports
      - name: ${{ matrix.group }} ${{ matrix.name }} Suite
        uses: cypress-io/github-action@v4
        with:
          browser: chrome
          record: true
          tag: ${{ matrix.group }} ${{ matrix.tag }}
          spec: ${{ matrix.filePath }}
        env:
          # pass the Dashboard record key as an environment variable
          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
          CYPRESS_PROJECT_ID: ${{ secrets.PROJECT_ID }}
          CYPRESS_STORE_ID: ${{ matrix.storeId }}
          CYPRESS_BACKUP_STORE_ID: ${{ matrix.backupStoreId }}
          CYPRESS_COUNTRY_HEADER: ${{ matrix.countryHeader }}
          CYPRESS_COUNTRY_SHORT_CODE: ${{ matrix.country }}
          # Recommended: pass the GitHub token lets this action correctly
          # determine the unique run id necessary to re-run the checks
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: Copy Screenshots
        run: yarn copy:screenshots
        if: failure()
      - name: Send Screenshot
        run: yarn alert:slack
        if: failure()
      - name: Slack Notify
        uses: rtCamp/action-slack-notify@v2.2.0
        if: failure()
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          SLACK_TITLE: ${{ matrix.group }} ${{ matrix.name }} ${{ matrix.storeId }}
          SLACK_COLOR: ${{ job.status }}
          SLACK_MESSAGE: 'Automation Failure!'
          SLACK_USERNAME: qaAutomation
      - name: Upload Logs
        uses: actions/upload-artifact@v2
        if: always()
        with:
          name: API Logs ${{ matrix.name }}
          path: logs
      - name: generate report
        if: always()
        run: yarn generate:report
      - name: Upload Report
        uses: actions/upload-artifact@v2
        if: always()
        with:
          name: ${{ matrix.name }} ${{ matrix.storeId }} Report
          path: cypress/reports/mochareports/

I think its important to note the set matrix step:

steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Set Matrix
        id: set-matrix
        run: |
          SPECS="`jq -r --arg SPECS "$SPECS" '.include |= map(select([.group] | inside(["misc"])))' workflow-matrix/matrix.json`"
          echo "matrix="$SPECS"" >> $GITHUB_OUTPUT

This uses jq to filter your matrix and make this job only run test that are in the “group” of “misc”.

Its also important to note that I’m able to reference the properties I defined in my matrix earlier in my workflow files like so:

   env:
          CYPRESS_STORE_ID: ${{ matrix.storeId }}
          CYPRESS_BACKUP_STORE_ID: ${{ matrix.backupStoreId }}
          CYPRESS_COUNTRY_HEADER: ${{ matrix.countryHeader }}
          CYPRESS_COUNTRY_SHORT_CODE: ${{ matrix.country }}