diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index f20c7f2c8..7254f8b49 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -27,7 +27,7 @@ jobs: name: "Build list of E2E test projects" runs-on: ubuntu-latest outputs: - e2e-test-projects: ${{ steps.e2e-targets.outputs.e2e-test-projects }} + e2e-test-matrix: ${{ steps.e2e-targets.outputs.e2e-test-matrix }} steps: - name: Check out code uses: actions/checkout@v6 @@ -39,26 +39,100 @@ jobs: with: node-version-file: ".nvmrc" cache: "pnpm" + - name: Cache node_modules + id: nm-cache + uses: actions/cache@v4 + with: + path: | + node_modules + **/node_modules + key: pnpm-nm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'pnpm-workspace.yaml') }} - name: Install deps - run: pnpm install --frozen-lockfile - - name: Create list of E2E test projects + if: steps.nm-cache.outputs.cache-hit != 'true' + run: pnpm install --frozen-lockfile --prefer-offline + - name: Build E2E matrix id: e2e-targets + # Per-project shard count. Projects with count=1 run as a single job + # with --shard=1/1 (a no-op in Playwright) — for suites where the + # per-job setup overhead outweighs the parallelism win. Projects not + # listed here use the default count. + env: + SHARD_COUNTS: | + { + "@evervault/browser-e2e-tests": 1, + "@repo/crypto-harness-e2e-tests": 1 + } + DEFAULT_SHARDS: 2 + run: |- + E2E_PROJECTS=$(pnpm m ls --depth=1 --json | jq -cr '[.[] | select(.name|endswith("e2e-tests")) | .name]') + MATRIX=$(jq -cn \ + --argjson projects "$E2E_PROJECTS" \ + --argjson counts "$SHARD_COUNTS" \ + --argjson default_shards "$DEFAULT_SHARDS" \ + '[$projects[] as $p | + ($counts[$p] // $default_shards) as $n | + range(1; $n + 1) | {project: $p, shard: ., "total-shards": $n} + ]') + echo "e2e-test-matrix=$MATRIX" >> $GITHUB_OUTPUT + build: + name: "Build packages for E2E" + runs-on: ubuntu-latest + env: + VITE_API_URL: ${{ inputs.vite-api-url }} + VITE_EVERVAULT_JS_URL: ${{ inputs.vite-evervault-js-url }} + VITE_KEYS_URL: ${{ inputs.vite-keys-url }} + VITE_GOOGLE_PAY_MERCHANT_ID: ${{ vars.GOOGLE_PAY_MERCHANT_ID }} + VITE_TEST_COVERAGE: "${{ inputs.run-code-coverage-checks }}" + steps: + - name: Check out code + uses: actions/checkout@v6 + - uses: pnpm/action-setup@v6 + with: + run_install: false + - name: Setup Node.js environment + uses: actions/setup-node@v6 + with: + node-version-file: ".nvmrc" + cache: "pnpm" + - name: Cache node_modules + id: nm-cache + uses: actions/cache@v4 + with: + path: | + node_modules + **/node_modules + key: pnpm-nm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'pnpm-workspace.yaml') }} + - name: Install deps + if: steps.nm-cache.outputs.cache-hit != 'true' + run: pnpm install --frozen-lockfile --prefer-offline + - name: Build E2E dependencies + run: pnpm run --filter="{./e2e-tests/*}..." --if-present build + - name: Tar dist outputs run: |- - E2E_TEST_PROJECTS=$(pnpm m ls --depth=1 --json | jq -cr '[.[] | select(.name|endswith("e2e-tests")) | .name]') - echo "e2e-test-projects=$E2E_TEST_PROJECTS" >> $GITHUB_OUTPUT - # The e2e tests run for quite a long time. Until we can accurately select the appropriate tests to run, we're running them in parallel (2 jobs per project, each with 3-4 worker processes.) + find packages -type d -name dist -not -path "*/node_modules/*" -print0 \ + | tar --null -czf dist-outputs.tar.gz -T - + - name: Upload dist artifact + uses: actions/upload-artifact@v7 + with: + name: e2e-dist-outputs + path: dist-outputs.tar.gz + retention-days: 1 + if-no-files-found: error + # Larger e2e suites (inputs, ui-components) are split into 2 shards across + # separate runners; each shard runs Playwright with workers: "100%" so on an + # 8-core ev-runner-large it uses ~8 worker processes per shard. Smaller + # suites (browser, crypto-harness) run as a single job — setup overhead + # would otherwise dominate. The install + build is done once in the `build` + # job above and consumed here as an artifact. e2e-test: - name: "Run E2E Tests for ${{ matrix.project }} (Shard ${{ matrix.shard }}/${{ matrix.total-shards }})" - needs: [get-e2e-targets] + name: "Run E2E Tests for ${{ matrix.project }} (${{ matrix.shard }}/${{ matrix.total-shards }})" + needs: [get-e2e-targets, build] strategy: fail-fast: false matrix: - project: ${{ fromJson(needs.get-e2e-targets.outputs.e2e-test-projects) }} - shard: [1, 2] - include: - - total-shards: 2 + include: ${{ fromJson(needs.get-e2e-targets.outputs.e2e-test-matrix) }} timeout-minutes: 25 - runs-on: ubuntu-latest + runs-on: ev-runner-large container: image: mcr.microsoft.com/playwright:v1.57.0-noble permissions: @@ -90,10 +164,23 @@ jobs: with: node-version-file: ".nvmrc" cache: "pnpm" + - name: Cache node_modules + id: nm-cache + uses: actions/cache@v4 + with: + path: | + node_modules + **/node_modules + key: pnpm-nm-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', '.npmrc', 'pnpm-workspace.yaml') }} - name: Install deps + if: steps.nm-cache.outputs.cache-hit != 'true' run: pnpm install --frozen-lockfile --prefer-offline - - name: Build ${{ matrix.project }} - run: pnpm run -r --filter="${{ matrix.project }}..." --if-present build + - name: Download dist artifact + uses: actions/download-artifact@v7 + with: + name: e2e-dist-outputs + - name: Extract dist outputs + run: tar -xzf dist-outputs.tar.gz && rm dist-outputs.tar.gz - name: E2E test id: run-e2e-test if: ${{ inputs.update-screenshots == false }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 70250364c..1566b5068 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,6 +14,11 @@ on: description: "API endpoint" required: true type: string + base-ref: + description: "Git branch to diff against for incremental lint/test (e.g. 'master'). Leave empty to run on all packages." + required: false + type: string + default: "" jobs: lint: name: "Lint and Test" @@ -32,6 +37,9 @@ jobs: steps: - name: Check out code uses: actions/checkout@v6 + with: + # Needed so pnpm's [] filter can resolve the base ref locally. + fetch-depth: 0 - uses: pnpm/action-setup@v6 - name: Setup Node.js environment uses: actions/setup-node@v6 @@ -40,13 +48,41 @@ jobs: cache: "pnpm" - name: Install deps run: pnpm install --frozen-lockfile + - name: Resolve pnpm filter + # When base-ref is provided, lint/test only the packages changed since + # that ref plus their dependents (so consumers of a changed package + # still get checked). Root-level changes (lockfile, .npmrc, workflow + # files, root package.json) force a full run since pnpm's [since] + # filter only tracks files inside workspace packages. + run: |- + BASE="${{ inputs.base-ref }}" + GLOBAL_RE='^(pnpm-lock\.yaml|package\.json|\.npmrc|pnpm-workspace\.yaml|\.github/)' + if [ -n "$BASE" ]; then + REF="origin/$BASE" + if ! git rev-parse --verify "$REF" >/dev/null 2>&1; then + REF="$BASE" + fi + if git diff --name-only "$REF"...HEAD | grep -qE "$GLOBAL_RE"; then + echo "Global config changed since $REF — running on all packages" + echo "PNPM_FILTER_LINT=--filter=@evervault/*" >> $GITHUB_ENV + echo "PNPM_FILTER_TEST=-r" >> $GITHUB_ENV + else + echo "Filtering to packages changed since $REF (and their dependents)" + echo "PNPM_FILTER_LINT=--filter=...[$REF]" >> $GITHUB_ENV + echo "PNPM_FILTER_TEST=--filter=...[$REF]" >> $GITHUB_ENV + fi + else + echo "No base-ref provided — running on all packages" + echo "PNPM_FILTER_LINT=--filter=@evervault/*" >> $GITHUB_ENV + echo "PNPM_FILTER_TEST=-r" >> $GITHUB_ENV + fi - name: Formats check - run: pnpm run -r --filter="@evervault/*" --if-present format:check + run: pnpm run "$PNPM_FILTER_LINT" --if-present format:check - name: Build run: pnpm run -r --if-present build - name: Typescript check - run: pnpm run -r --filter="@evervault/*" --if-present typecheck + run: pnpm run "$PNPM_FILTER_LINT" --if-present typecheck - name: eslint check - run: pnpm run -r --filter="@evervault/*" --if-present lint + run: pnpm run "$PNPM_FILTER_LINT" --if-present lint - name: Run unit tests - run: pnpm -r --if-present test \ No newline at end of file + run: pnpm "$PNPM_FILTER_TEST" --if-present test diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 90cc4ef10..df41d3a66 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -18,6 +18,7 @@ jobs: vite-evervault-js-url: "https://js.evervault.io/v2" vite-keys-url: "https://keys.evervault.io" vite-api-url: "https://api.evervault.io" + base-ref: ${{ github.base_ref }} secrets: inherit e2e-test: uses: ./.github/workflows/e2e-test.yml diff --git a/.github/workflows/react-native.yml b/.github/workflows/react-native.yml index 232dc11d0..2638a2eb2 100644 --- a/.github/workflows/react-native.yml +++ b/.github/workflows/react-native.yml @@ -5,9 +5,12 @@ on: paths: - "examples/expo/**" - "packages/react-native-v2/**" - - ".github/**" + - ".github/workflows/react-native.yml" - "pnpm-lock.yaml" - "pnpm-workspace.yaml" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build_ios: name: Build iOS Examples diff --git a/.npmrc b/.npmrc index 8e5a554b4..373a7c6da 100644 --- a/.npmrc +++ b/.npmrc @@ -1,2 +1,4 @@ node-linker=hoisted shamefully-hoist=true +network-concurrency=16 +verify-store-integrity=false diff --git a/e2e-tests/inputs/tests/events.spec.js b/e2e-tests/inputs/tests/events.spec.js index 9df8036e0..9e64344be 100644 --- a/e2e-tests/inputs/tests/events.spec.js +++ b/e2e-tests/inputs/tests/events.spec.js @@ -32,7 +32,7 @@ test.describe("evervault inputs", () => { .fill(CardLib.validExpirationData); await page.getByLabel("Security code").fill(CardLib.validSecurityCode); - await page.waitForTimeout(800); + await expect.poll(() => data?.isValid).toBe(true); expect(calls).toBeGreaterThan(0); expect(data.encryptedCard.number).toMatch(EV_STRING_REGEX); @@ -73,7 +73,7 @@ test.describe("evervault inputs", () => { .fill(CardLib.validExpirationData); await page.getByLabel("Security code").fill(CardLib.validAmexCVV); - await page.waitForTimeout(800); + await expect.poll(() => data?.isValid).toBe(true); expect(calls).toBeGreaterThan(0); expect(data.encryptedCard.number).toMatch(EV_STRING_REGEX); diff --git a/e2e-tests/inputs/tests/magswipe.spec.js b/e2e-tests/inputs/tests/magswipe.spec.js index 14d7c43a3..aae87deb7 100644 --- a/e2e-tests/inputs/tests/magswipe.spec.js +++ b/e2e-tests/inputs/tests/magswipe.spec.js @@ -34,7 +34,7 @@ test.describe("evervault inputs", () => { ); await page.getByLabel("Card number").press("Enter"); - await page.waitForTimeout(500); + await expect.poll(() => data?.encryptedCard?.swipe).toBe(true); expect(calls).toBeGreaterThan(0); expect(data.encryptedCard.bin).toMatch("424242"); @@ -80,7 +80,7 @@ test.describe("evervault inputs", () => { .type(";4242424242424242=3001123456?"); await page.getByLabel("Card number").press("Enter"); - await page.waitForTimeout(500); + await expect.poll(() => data?.encryptedCard?.swipe).toBe(true); expect(calls).toBeGreaterThan(0); diff --git a/e2e-tests/ui-components/tests/cardDetails.spec.js b/e2e-tests/ui-components/tests/cardDetails.spec.js index 62721f05b..491221d86 100644 --- a/e2e-tests/ui-components/tests/cardDetails.spec.js +++ b/e2e-tests/ui-components/tests/cardDetails.spec.js @@ -727,8 +727,8 @@ test.describe("card component", () => { card.mount("#form"); }); - await page.waitForTimeout(2000); const frame = page.frameLocator("iframe[data-evervault]"); + await expect(frame.getByLabel("Number")).toBeVisible(); await frame.getByLabel("Number").fill("4242424242424242"); await frame.getByLabel("Expiration").fill("12"); // intentionally incomplete await frame.getByLabel("Expiration").blur(); @@ -744,8 +744,8 @@ test.describe("card component", () => { card.mount("#form"); }); - await page.waitForTimeout(2000); const frame = page.frameLocator("iframe[data-evervault]"); + await expect(frame.getByLabel("Number")).toBeVisible(); await frame.getByLabel("Number").fill("4242424242424242"); await frame.getByLabel("Expiration").fill("12"); // intentionally incomplete await frame.getByLabel("Expiration").blur(); @@ -761,8 +761,8 @@ test.describe("card component", () => { card.mount("#form"); }); - await page.waitForTimeout(2000); const frame = page.frameLocator("iframe[data-evervault]"); + await expect(frame.getByLabel("Number")).toBeVisible(); await frame.getByLabel("Number").fill("4242424242424242"); await frame.getByLabel("Expiration").fill("12"); // intentionally incomplete await frame.getByLabel("Expiration").blur(); @@ -775,8 +775,8 @@ test.describe("card component", () => { card.mount("#form"); }, inlineTheme); - await page.waitForTimeout(2000); const frame = page.frameLocator("iframe[data-evervault]"); + await expect(frame.getByLabel("Number")).toBeVisible(); await frame.getByLabel("Number").fill("4242424242424242"); await frame.getByLabel("Expiration").fill("12"); // intentionally incomplete await frame.getByLabel("Expiration").blur();