e2e.yml 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. name: End-to-end tests
  2. on:
  3. pull_request:
  4. branches: [main]
  5. workflow_dispatch:
  6. # A second push to the same branch makes the run in flight pointless.
  7. concurrency:
  8. group: e2e-${{ github.ref }}
  9. cancel-in-progress: true
  10. # The suite runs one test at a time against one seeded database, so more
  11. # workers would only trip over each other. More databases do not: resetting
  12. # and seeding one takes about fifteen seconds. So the specs are split across
  13. # shards, each on its own runner with its own database, and the cloud specs
  14. # run beside them. The last job merges what they found into one report and is
  15. # the one check a pull request waits on.
  16. jobs:
  17. e2e:
  18. name: Playwright (${{ matrix.name }})
  19. runs-on: ubuntu-latest
  20. timeout-minutes: 30
  21. strategy:
  22. # A failure in one shard says nothing about the others; let them finish.
  23. fail-fast: false
  24. matrix:
  25. include:
  26. - { id: shard-1, name: shard 1 of 8, shard: 1/8 }
  27. - { id: shard-2, name: shard 2 of 8, shard: 2/8 }
  28. - { id: shard-3, name: shard 3 of 8, shard: 3/8 }
  29. - { id: shard-4, name: shard 4 of 8, shard: 4/8 }
  30. - { id: shard-5, name: shard 5 of 8, shard: 5/8 }
  31. - { id: shard-6, name: shard 6 of 8, shard: 6/8 }
  32. - { id: shard-7, name: shard 7 of 8, shard: 7/8 }
  33. - { id: shard-8, name: shard 8 of 8, shard: 8/8 }
  34. # The same build started in cloud mode: plan limits, the sign-up
  35. # pitch and Google sign-in only exist there.
  36. - { id: cloud, name: cloud, mode: cloud }
  37. services:
  38. postgres:
  39. image: postgres:16-alpine
  40. env:
  41. POSTGRES_USER: torqvoice
  42. POSTGRES_PASSWORD: torqvoice
  43. # The name has to carry "e2e" or "test": the harness refuses to reset
  44. # a database whose name says nothing about being throwaway.
  45. POSTGRES_DB: torqvoice_e2e
  46. ports:
  47. - 5432:5432
  48. options: >-
  49. --health-cmd "pg_isready -U torqvoice -d torqvoice_e2e"
  50. --health-interval 10s
  51. --health-timeout 5s
  52. --health-retries 5
  53. env:
  54. # Playwright starts the app itself on this port and resets this database
  55. # before it does; both come from playwright.config.ts.
  56. E2E_DATABASE_URL: postgresql://torqvoice:torqvoice@127.0.0.1:5432/torqvoice_e2e
  57. E2E_MODE: ${{ matrix.mode }}
  58. # The cloud token torqvoice.com minted for http://127.0.0.1:3100, the
  59. # origin this suite builds for; without it the app ignores
  60. # TORQVOICE_MODE=cloud. Only the cloud job gets it. A token and never the
  61. # signing key: this env reaches npm ci and every install script with it.
  62. E2E_CLOUD_TOKEN: ${{ matrix.mode == 'cloud' && secrets.E2E_CLOUD_TOKEN || '' }}
  63. steps:
  64. # A pull request from a fork gets no secrets, so the cloud job has no
  65. # cloud token and the app would refuse cloud mode. That says nothing
  66. # about the contribution, so the job skips instead of failing. The
  67. # self-hosted shards need no token and always run.
  68. - name: Check for the cloud token
  69. id: gate
  70. run: |
  71. if [ "${{ matrix.mode }}" = "cloud" ] && [ -z "$E2E_CLOUD_TOKEN" ]; then
  72. echo "::notice title=Cloud specs skipped::No E2E_CLOUD_TOKEN here (a fork or Dependabot pull request), so the cloud specs cannot run."
  73. echo "run=false" >> "$GITHUB_OUTPUT"
  74. else
  75. echo "run=true" >> "$GITHUB_OUTPUT"
  76. fi
  77. - uses: actions/checkout@v4
  78. if: ${{ steps.gate.outputs.run == 'true' }}
  79. - uses: actions/setup-node@v4
  80. if: ${{ steps.gate.outputs.run == 'true' }}
  81. with:
  82. node-version: 22.23.4
  83. cache: npm
  84. # A fresh one per run: the sessions it signs live as long as the job, and
  85. # a short or guessable value makes better-auth warn on every request.
  86. - name: Make a session secret for this run
  87. if: ${{ steps.gate.outputs.run == 'true' }}
  88. run: echo "BETTER_AUTH_SECRET=$(openssl rand -base64 32)" >> "$GITHUB_ENV"
  89. - run: npm ci
  90. if: ${{ steps.gate.outputs.run == 'true' }}
  91. - run: npx prisma generate
  92. if: ${{ steps.gate.outputs.run == 'true' }}
  93. # Keyed on the pinned version, because the image tag and this download
  94. # have to be the same build.
  95. - name: Cache the browser
  96. if: ${{ steps.gate.outputs.run == 'true' }}
  97. uses: actions/cache@v4
  98. with:
  99. path: ~/.cache/ms-playwright
  100. key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
  101. - name: Install the browser
  102. if: ${{ steps.gate.outputs.run == 'true' }}
  103. run: npx playwright install --with-deps chromium
  104. # The seed copies bundled vehicle photos when they are in the tree and
  105. # downloads fifty of them from Unsplash when they are not, one after
  106. # another, which is minutes of a cold run and the flakiest thing in it.
  107. # Cached, a warm run copies them off disk instead.
  108. - name: Cache the seed's photos
  109. if: ${{ steps.gate.outputs.run == 'true' }}
  110. uses: actions/cache@v4
  111. with:
  112. # Where prepare-db.ts points the seed's DATA_ROOT, so a test run
  113. # cannot disturb a running instance's own uploads.
  114. path: e2e/.data
  115. key: seed-photos-${{ hashFiles('prisma/seed_dummy_data.ts') }}
  116. restore-keys: |
  117. seed-photos-
  118. # Next reuses its compiler cache across builds when it is given one.
  119. # Every job reads it; only the first shard writes it back, so five jobs
  120. # do not race to save the same entry.
  121. - name: Restore the build cache
  122. if: ${{ steps.gate.outputs.run == 'true' }}
  123. uses: actions/cache/restore@v4
  124. with:
  125. path: .next/cache
  126. key: next-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
  127. restore-keys: |
  128. next-${{ hashFiles('package-lock.json') }}-
  129. next-
  130. # NEXT_PUBLIC_APP_URL is baked into the client bundle, and better-auth
  131. # refuses a sign-in from an origin the build was not made for, so it has
  132. # to match the base URL the suite uses.
  133. - name: Build
  134. if: ${{ steps.gate.outputs.run == 'true' }}
  135. run: npm run build
  136. env:
  137. NEXT_PUBLIC_APP_URL: http://127.0.0.1:3100
  138. - name: Save the build cache
  139. if: ${{ steps.gate.outputs.run == 'true' && (matrix.id == 'shard-1') }}
  140. uses: actions/cache/save@v4
  141. with:
  142. path: .next/cache
  143. key: next-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
  144. - name: Run the specs
  145. if: ${{ steps.gate.outputs.run == 'true' }}
  146. run: npx playwright test ${{ matrix.shard && format('--shard={0}', matrix.shard) || '' }}
  147. # The blob carries the results with their traces, screenshots and videos;
  148. # the report job turns every job's blob into one HTML report.
  149. - name: Upload the blob report
  150. if: ${{ steps.gate.outputs.run == 'true' && (!cancelled()) }}
  151. uses: actions/upload-artifact@v4
  152. with:
  153. name: blob-report-${{ matrix.id }}
  154. path: blob-report/
  155. retention-days: 1
  156. if-no-files-found: ignore
  157. report:
  158. # Named as the single job used to be, so a required check keeps its name.
  159. name: Playwright
  160. needs: e2e
  161. if: ${{ !cancelled() }}
  162. runs-on: ubuntu-latest
  163. timeout-minutes: 10
  164. steps:
  165. - uses: actions/checkout@v4
  166. - uses: actions/setup-node@v4
  167. with:
  168. node-version: 22.23.4
  169. cache: npm
  170. - run: npm ci
  171. - name: Download the blob reports
  172. uses: actions/download-artifact@v4
  173. with:
  174. pattern: blob-report-*
  175. path: all-blob-reports
  176. # Each job's blob lands in a folder of its own. The merge wants them side
  177. # by side, and two jobs may give their file the same name.
  178. - name: Gather the blobs
  179. run: |
  180. mkdir -p blob-reports
  181. for dir in all-blob-reports/*/; do
  182. [ -d "$dir" ] || continue
  183. job=$(basename "$dir")
  184. for file in "$dir"*.zip; do
  185. [ -e "$file" ] || continue
  186. mv "$file" "blob-reports/${job}-$(basename "$file")"
  187. done
  188. done
  189. ls -la blob-reports
  190. - name: Merge the reports
  191. run: npx playwright merge-reports --reporter html ./blob-reports
  192. - name: Upload the report
  193. uses: actions/upload-artifact@v4
  194. with:
  195. name: playwright-report
  196. path: playwright-report/
  197. retention-days: 7
  198. if-no-files-found: ignore
  199. # Green only when every shard and the cloud specs are.
  200. - name: Every job passed
  201. if: ${{ always() }}
  202. run: |
  203. if [ "${{ needs.e2e.result }}" != "success" ]; then
  204. echo "The e2e jobs finished as: ${{ needs.e2e.result }}"
  205. exit 1
  206. fi