diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 22df08241c05..29ca1377b271 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -3,24 +3,9 @@ name: E2E Tests on: workflow_call: secrets: - PLAYWRIGHT_SERVICE_URL: - required: false - PLAYWRIGHT_SERVICE_ACCESS_TOKEN: - required: false ENTERPRISE_LICENSE_KEY: required: true - # A secret added here must also be named in pr.yml's e2e-test job: the caller does not - # `inherit`, so an unnamed optional secret arrives empty and a required one fails the call. workflow_dispatch: - inputs: - force_mode: - description: "Playwright execution mode (auto = use the service when its secrets are set)" - type: choice - options: - - auto - - service - - local - default: auto permissions: contents: read @@ -30,16 +15,10 @@ jobs: build: name: Run E2E Tests runs-on: ubuntu-latest - # Chosen so the step timeouts below always fire FIRST, because a step timeout names - # the culprit while a job timeout just says "exceeded the maximum execution time" — - # the opaque symptom this workflow is being changed to eliminate. - # - # Measured fixed overhead outside the two capped steps is 122s (from the local-mode - # run 31169882088: 1009s total - 247s build - 640s tests), so the worst case where - # both step caps are hit is 122 + 720 + 900 = 1742s ~= 29m. 30 clears that; 25 did - # not, and on the fork path would have killed the job before either step timeout. - # Still half the previous 60, which let a stalled `playwright install` sit on the - # merge queue for the better part of an hour. + permissions: + contents: read + actions: read + id-token: write timeout-minutes: 30 services: postgres: @@ -76,46 +55,6 @@ jobs: persist-credentials: false - uses: ./.github/actions/dangerous-git-checkout - # Resolved before anything expensive runs, because it decides whether local browser - # binaries are needed at all. In service mode the browsers execute on Azure, so the - # apt-backed `playwright install` further down is pure waste — and that apt step has - # stalled for 40+ minutes on the Ubuntu mirror, wedging the merge queue behind a job - # whose build had long since finished. - - name: Determine Playwright execution mode - shell: bash - env: - # `inputs` is empty when this workflow is reached via workflow_call, so the - # fallback keeps the automatic secret-based detection for PR/merge_group runs. - FORCE_MODE: ${{ inputs.force_mode || 'auto' }} - PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }} - PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }} - run: | - set -euo pipefail - - if [[ "${FORCE_MODE}" == "local" ]]; then - echo "PW_MODE=local" >> "$GITHUB_ENV" - echo "Playwright mode forced to 'local' via workflow_dispatch input." - exit 0 - fi - - if [[ "${FORCE_MODE}" == "service" ]]; then - if [[ -z "${PLAYWRIGHT_SERVICE_URL}" || -z "${PLAYWRIGHT_SERVICE_ACCESS_TOKEN}" ]]; then - echo "::error::force_mode=service but PLAYWRIGHT_SERVICE_URL / PLAYWRIGHT_SERVICE_ACCESS_TOKEN are not both set." - exit 1 - fi - echo "PW_MODE=service" >> "$GITHUB_ENV" - echo "Playwright mode forced to 'service' via workflow_dispatch input." - exit 0 - fi - - if [[ -n "${PLAYWRIGHT_SERVICE_URL}" && -n "${PLAYWRIGHT_SERVICE_ACCESS_TOKEN}" ]]; then - echo "PW_MODE=service" >> "$GITHUB_ENV" - echo "Playwright mode: service (both service secrets present)." - else - echo "PW_MODE=local" >> "$GITHUB_ENV" - echo "Playwright mode: local (service secrets absent — fork PR or unconfigured repo)." - fi - - name: Install pnpm uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 @@ -197,101 +136,12 @@ jobs: # regardless; a real reduction needs a Turbo cache backend, not local # actions/cache. - # Cache the downloaded browser binaries so re-runs skip the ~100MB - # Chromium download. `--with-deps` below still runs the (uncacheable) apt - # step, but a cache hit avoids the browser fetch itself. - # Local mode only — service-mode runs never download a browser to cache. - # NOTE: Actions caches are branch-scoped and every merge_group run happens on a - # throwaway `gh-readonly-queue/...` branch, so this misses ~always there. It still - # earns its keep on repeat runs of the same PR branch. - - name: Cache Playwright browsers - if: env.PW_MODE == 'local' - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - with: - path: ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-playwright- - - - name: Build App (with Playwright browser install in parallel when needed) + - name: Build App timeout-minutes: 12 env: - # Turborepo hashes its own process environment, not the `.env` file this value - # is otherwise written to. E2E_TESTING changes the emitted CSP - # (apps/web/next.config.mjs), so it has to be visible here — otherwise any - # shared or remote Turbo cache would happily replay the non-E2E build for this - # job. No behavioural change today: Next already reads the same value via dotenv. E2E_TESTING: "1" - run: | - set -euo pipefail - - PW_INSTALL_PID="" - - emit_install_log() { - echo "--- playwright install output ---" - cat /tmp/playwright-install.log 2>/dev/null || echo "(no output captured)" - echo "--- end playwright install output ---" - } - - # If `pnpm build` fails, `set -e` leaves the step before the reaping block at the - # bottom — stranding the installer and discarding its log, which is exactly the - # diagnostic this step exists to preserve. The trap reaps the child and emits the - # log on every exit path; the happy path clears PW_INSTALL_PID once it has done - # both itself, so this never runs twice. - on_exit() { - local status=$? - if [[ -n "${PW_INSTALL_PID}" ]]; then - kill "${PW_INSTALL_PID}" 2>/dev/null || true - wait "${PW_INSTALL_PID}" 2>/dev/null || true - emit_install_log - fi - return "${status}" - } - trap on_exit EXIT - - if [[ "${PW_MODE:-}" == "local" ]]; then - # Install Playwright browsers + OS deps concurrently with the Next.js build. - # Both are largely I/O-bound, so overlapping them shaves the browser-install - # time (~1-2 min) off the critical path. - # - # Only `chromium` is enabled in playwright.config.ts (firefox/webkit are - # commented out), and `ffmpeg` is a separate download that the config's - # `video: "retain-on-failure"` needs — naming neither would pull Firefox and - # WebKit for nothing, naming only chromium would silently break failure videos. - # - # Bounded with `timeout` and logged to a file that is always echoed below: - # the `--with-deps` apt step has hung for 40+ minutes on the Ubuntu mirror, - # and because it is backgrounded that looked like a hung build with no output. - # 420s is ~5x the observed healthy install; it runs concurrently with the - # ~247s build, so it only becomes this step's critical path when it misbehaves. - (timeout 420 pnpm exec playwright install chromium ffmpeg --with-deps \ - > /tmp/playwright-install.log 2>&1) & - PW_INSTALL_PID=$! - else - echo "PW_MODE=${PW_MODE:-unset}: browsers run on the Playwright service, skipping local install." - fi - - pnpm build --filter=@formbricks/web... - - if [[ -n "${PW_INSTALL_PID}" ]]; then - # `wait` yields the background subshell's exit status. Capture it via `|| rc=$?` - # rather than a temp file: under `set -e` the subshell dies the moment the - # install fails, so anything appended after the install inside it never runs. - pw_rc=0 - wait "${PW_INSTALL_PID}" || pw_rc=$? - PW_INSTALL_PID="" # reaped — keep the EXIT trap from re-reporting it - - emit_install_log - - if [[ "${pw_rc}" == "124" ]]; then - echo "::error::playwright install exceeded its 420s budget (apt mirror stall?). See the install output above." - exit 1 - fi - if [[ "${pw_rc}" != "0" ]]; then - echo "::error::playwright install failed with exit code ${pw_rc}. See the install output above." - exit 1 - fi - fi + run: pnpm build + shell: bash - name: Apply Prisma Migrations run: | @@ -386,23 +236,11 @@ jobs: exit 1 shell: bash - - name: Run E2E Tests (Playwright Service) - if: env.PW_MODE == 'service' + - name: Run E2E Tests (Endform) timeout-minutes: 15 env: - PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }} - PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }} CI: true - run: | - pnpm test-e2e:azure - - - name: Run E2E Tests (Local) - if: env.PW_MODE == 'local' - timeout-minutes: 15 - env: - CI: true - run: | - pnpm test:e2e + run: npx endform@latest test --organization-id 2G1ZCj7X - name: Verify AuthZed CI outbox worker if: always() && steps.authzed-worker-start.outcome == 'success' diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 63cd6b482e8b..80842eb080d0 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -1,7 +1,6 @@ name: PR Update -# No `id-token: write` here: nothing in this gate uses OIDC (the Azure login that needed it left -# e2e.yml in #6949), and a workflow-level grant reaches every job, most of which run PR-head code. +# Endform uses OIDC only in e2e-test; keep that permission scoped to its caller job. permissions: contents: read pull-requests: read @@ -51,10 +50,11 @@ jobs: e2e-test: name: Run E2E Tests uses: ./.github/workflows/e2e.yml - # The three e2e.yml declares, named rather than inherited — see api-v3-contract-tests below. + permissions: + contents: read + actions: read + id-token: write secrets: - PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }} - PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }} ENTERPRISE_LICENSE_KEY: ${{ secrets.ENTERPRISE_LICENSE_KEY }} validate-authzed-schema: diff --git a/endform.config.ts b/endform.config.ts new file mode 100644 index 000000000000..c9437b07f873 --- /dev/null +++ b/endform.config.ts @@ -0,0 +1,15 @@ +import { defineEndformConfig } from "endform"; + +export default defineEndformConfig({ + // The app and S3 service are started by CI outside Playwright's webServer. + proxyNetworkHosts: [""], + // Test fixtures seed the same CI-local Postgres database through Prisma. + proxyNetworkPorts: [Number(process.env.POSTGRES_PORT || 5432)], + environmentVariables: ["^DATABASE_URL$"], + additionalFiles: [ + "apps/web/public/logo-transparent.png", + "apps/web/public/favicon/android-chrome-192x192.png", + ], + // Keep application pressure at the established native runner's four workers. + concurrentTestLimits: [{ scope: "within-suite-run", limit: 4 }], +}); diff --git a/package.json b/package.json index 8d98493e212b..d0429fad0a70 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "@redocly/cli": "1.34.17", "@trivago/prettier-plugin-sort-imports": "catalog:", "dotenv": "catalog:", + "endform": "0.81.1", "eslint": "9.39.5", "husky": "9.1.7", "js-yaml": "catalog:", diff --git a/playwright.config.ts b/playwright.config.ts index 404a43054afd..ee3554ef1a3c 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,10 +1,11 @@ import { defineConfig, devices } from "@playwright/test"; +import { config } from "dotenv"; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ -require("dotenv").config({ path: ".env" }); +config({ path: ".env" }); /** * See https://playwright.dev/docs/test-configuration. @@ -29,10 +30,10 @@ export default defineConfig({ /* Shared settings for all the workspaces below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: "http://localhost:3000", + baseURL: process.env.BASE_URL || "http://localhost:3000", - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", + /* Retain traces for failed attempts, including failures before a retry. */ + trace: "retain-on-failure", permissions: ["clipboard-read", "clipboard-write"], screenshot: "only-on-failure", // Capture screenshots only on test failure video: "retain-on-failure", // Optionally record video on failure diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bc69ab1214f..ec9aa63f2040 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,6 +270,9 @@ importers: dotenv: specifier: 'catalog:' version: 17.3.1 + endform: + specifier: 0.81.1 + version: 0.81.1 eslint: specifier: 9.39.5 version: 9.39.5(jiti@2.7.0) @@ -7686,6 +7689,40 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + endform-darwin-arm64@0.81.1: + resolution: {integrity: sha512-duBYoWEdBEdk9UHUrUA9ZYP3RqxXGrjS6plkrwFmdnhFAph+6gopXUljw6d1pJB0j/kP25LPBEPK+5iQ1rx5ng==} + cpu: [arm64] + os: [darwin] + + endform-darwin-x86@0.81.1: + resolution: {integrity: sha512-an6GjutD6PZAF5PLlWIEWTMOkLZx5LrVZ4OmukL1eQd8WWITsFi42VGfxYpYJFXe7GdQaCDrEaJSLCdsL0Iqyw==} + cpu: [x64] + os: [darwin] + + endform-linux-arm64@0.81.1: + resolution: {integrity: sha512-gxhTi3jsRtzA4ZhibK4idNAcp0cb0V4idZkJxLCq9ao8FUMUiiVHhjhtiFNO7eUGgsA8+DfFnRdiPa2zTePWjQ==} + cpu: [arm64] + os: [linux] + + endform-linux-x86@0.81.1: + resolution: {integrity: sha512-aXZP8Wds4bzcCRMFOL64/B7jQLIP7ECrDW8H6vjl6TZTjg7lipfPLams8R2RDigRUjB5i8RDwuusoeOMvYf2gA==} + cpu: [x64] + os: [linux] + + endform-win32-arm64@0.81.1: + resolution: {integrity: sha512-ckWkuUgGj9bPzMfTJOLX2n6YgWjPxhJWtcKMQX6it5OhB8bDjHA2vnhOowPZlLoPwLtjT3NaEUnh81NdHFVhDw==} + cpu: [arm64] + os: [win32] + + endform-win32-x86@0.81.1: + resolution: {integrity: sha512-I5cXm6huxhcs5ZTH+U4O0Ehx6kvpKH4HVATPT5y4C+GliAZVY+rcrB2NBIl+61xWsjHmoC6tkn+HX6lzKJlCGw==} + cpu: [x64] + os: [win32] + + endform@0.81.1: + resolution: {integrity: sha512-dLWKcpN6FfIEcxzzlJkBBDLcxll7xkS36tuKadt13lM8ZwbcmytgLHHgJ1USLtLuUWCrsPq172qtiZHN3sufXQ==} + hasBin: true + engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} @@ -19409,6 +19446,33 @@ snapshots: dependencies: once: 1.4.0 + endform-darwin-arm64@0.81.1: + optional: true + + endform-darwin-x86@0.81.1: + optional: true + + endform-linux-arm64@0.81.1: + optional: true + + endform-linux-x86@0.81.1: + optional: true + + endform-win32-arm64@0.81.1: + optional: true + + endform-win32-x86@0.81.1: + optional: true + + endform@0.81.1: + optionalDependencies: + endform-darwin-arm64: 0.81.1 + endform-darwin-x86: 0.81.1 + endform-linux-arm64: 0.81.1 + endform-linux-x86: 0.81.1 + endform-win32-arm64: 0.81.1 + endform-win32-x86: 0.81.1 + engine.io-parser@5.2.3: {} engine.io@6.6.10: