squally-reporter 0.12.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,509 +1,505 @@
1
- # squally-reporter
2
-
3
- Playwright reporter that sends test results to your Squally dashboard.
4
-
5
- Learn more at [app.squally.dev](https://app.squally.dev).
6
-
7
- ## Requirements
8
-
9
- - Node.js >= 22.19.0 (required by the `undici` dependency)
10
- - `@playwright/test` >= 1.30.0
11
-
12
- ## Install
13
-
14
- ```bash
15
- npm install -D squally-reporter
16
- ```
17
-
18
- ## Config
19
-
20
- Set one env var (e.g. as a CI secret):
21
-
22
- ```bash
23
- SQUALLY_INGEST_KEY=your-project-ingest-key
24
- ```
25
-
26
- Find your ingest key in the Squally dashboard under **Settings › Project ›
27
- Ingest key**.
28
-
29
- **Since 0.12.0 the URL defaults to `https://app.squally.dev`.** Set
30
- `SQUALLY_INGEST_URL` only to report to another Squally. Without a key the
31
- reporter sends nothing and says so once.
32
-
33
- **Renamed in 0.9.0.** These were `SQUALLY_API_URL` and `SQUALLY_API_KEY`. The
34
- old names still work for now, with a deprecation warning in the CI log - but
35
- they belong to Squally's read API now, so rename them. `SQUALLY_API_URL` is
36
- only used together with `SQUALLY_API_KEY`, never on its own - and when it is
37
- set but not used, the reporter sends nothing rather than falling back to the
38
- default URL, since the variable says you meant another target.
39
-
40
- **A read key is never sent.** A key starting with `sqly_ro_` is a read API key,
41
- for scripts and agents; reporting needs the ingest key. Since 0.12.0 the
42
- reporter refuses one in any of `SQUALLY_INGEST_KEY`, `SQUALLY_API_KEY` or the
43
- `apiKey` option before sending anything, and says which one to fix.
44
-
45
- Then add the reporter to `playwright.config.ts`:
46
-
47
- ```ts
48
- import { defineConfig } from "@playwright/test";
49
-
50
- export default defineConfig({
51
- reporter: [
52
- ["list"],
53
- ["squally-reporter"],
54
- ],
55
- });
56
- ```
57
-
58
- Alternatively, pass config explicitly instead of using env vars - but **prefer
59
- the env vars above for `apiKey`, and not only because a literal in a committed
60
- file is obviously wrong.** Playwright's `json` and `blob` reporters serialize
61
- your resolved config into their output file, options and all: an `apiKey`
62
- passed here ends up in that file in plain text, even when its value came from
63
- an environment variable at runtime. What gets written is the option, not where
64
- it came from. The reporter warns, naming the file, when it sees this
65
- combination. Measured against a real report, 20.09.2026.
66
-
67
- ```ts
68
- reporter: [
69
- ["squally-reporter", { apiUrl: "https://app.squally.dev", apiKey: process.env.SQUALLY_INGEST_KEY }],
70
- ],
71
- ```
72
-
73
- That's it - run your tests as usual (`npx playwright test`); results stream
74
- to Squally while the suite runs (see *How results reach Squally* below).
75
-
76
- ## Artifact upload (screenshot, trace, browser console)
77
-
78
- If your `playwright.config.ts` captures screenshots or traces, the reporter
79
- uploads them to Squally so they show up next to the failing test in the
80
- dashboard - no separate setup beyond the two env vars above.
81
-
82
- Three kinds are uploaded:
83
-
84
- | Kind | Where it comes from |
85
- | --- | --- |
86
- | **Screenshot** | the file Playwright writes for a failed attempt |
87
- | **Trace** | the trace zip Playwright writes |
88
- | **Browser console** | read out of that trace zip by this reporter |
89
-
90
- **The browser console needs a trace.** It is not something Playwright hands a
91
- reporter: `result.stdout`/`stderr` are the output of the *Node* process - what
92
- your test file printed - while what the *page* printed only exists inside the
93
- trace. So the reporter opens the trace and reads it out, which means an
94
- attempt with no trace has no browser console, and nothing can change that but
95
- turning tracing on. Squally's attempt page says which of the two it is rather
96
- than showing an empty section.
97
-
98
- Supported trace schema versions: **5 to 10**, which covers Playwright 1.30
99
- through 1.62 (1.62 writes version 8). A trace outside that range is left alone
100
- and the reporter prints one warning naming the version - it never uploads an
101
- empty console file, because "no messages" and "this reporter could not read
102
- it" must not look the same in the dashboard.
103
-
104
- **Only from attempts worth looking at:** artifacts are uploaded only for
105
- attempts that **failed**, or that **needed a retry** (even if that retry then
106
- passed) - never for an attempt that passed on its first try, regardless of how
107
- `screenshot`/`trace` are configured. Nobody looks at a screenshot of a test
108
- that just passed, and uploading one anyway would only eat into your
109
- organization's storage for no benefit.
110
-
111
- **You need to actually enable capture in `playwright.config.ts`** - without
112
- it, Playwright doesn't produce any attachments, so there's nothing for the
113
- reporter to find or upload (no error either way, it's a silent no-op):
114
-
115
- ```ts
116
- export default defineConfig({
117
- use: {
118
- screenshot: "only-on-failure",
119
- trace: "retain-on-failure",
120
- },
121
- // ...
122
- });
123
- ```
124
-
125
- **Limits.** None of these are enforced by the reporter - Squally applies them
126
- when the upload is requested and answers with a reason, which is what the
127
- reporter prints. The numbers below are therefore Squally's current ones rather
128
- than anything this package guarantees, and can change server-side without a new
129
- release of it:
130
-
131
- - **50 MB per screenshot or trace, 4 MB per browser console**, the same on
132
- every plan. A larger file is rejected before any bytes are uploaded; the
133
- reporter logs a `console.warn` naming the file and its limit and moves on.
134
- The test result itself is reported as usual, and the run's outcome and exit
135
- code are untouched. In the dashboard that artifact is shown as skipped, with
136
- the reason, rather than silently missing.
137
- - **At most 250 artifacts per shard.** Squally keeps the first 250 and skips
138
- the rest with the reason `limit`, which the reporter explains in one line.
139
- - **A total storage cap per organization, depending on your Squally plan.**
140
- Once it is reached, further uploads are rejected the same way until you free
141
- up space or upgrade. Note that a fully instrumented suite now produces up to
142
- three artifacts per failed attempt rather than two.
143
- - The browser console is a text file of a few KB - it is extracted from the
144
- trace you are already uploading, so it adds no capture cost to your run.
145
- - **Your test's own `stdout`/`stderr` is cut at 16 KB** before it is sent. That
146
- is this reporter's own limit, not Squally's, and it applies whether or not
147
- you capture artifacts at all.
148
- - Two minutes per upload, and 30 seconds for the calls that surround it. An
149
- unreachable or very slow server means a skipped upload and a `console.warn`,
150
- not a CI job waiting indefinitely.
151
-
152
- Upload is best-effort and fully non-blocking, same principle as the rest of
153
- this reporter - a failed or skipped upload never fails your test run or
154
- changes its exit code, it only logs a `console.warn` explaining what
155
- happened.
156
-
157
- ## How results reach Squally
158
-
159
- Results stream to Squally as your suite runs, not all at once at the end:
160
- each test's result is sent in a small batch, so the "running" view in the
161
- dashboard stays close to real time even for a long suite. A batch goes out on
162
- whichever of these comes first:
163
-
164
- - **25 results.** Fixed, and the same whether you shard or not.
165
- - **A timer: 2 seconds, or 0.4 seconds × the number of shards when that is
166
- longer, capped at 30 seconds** (0.12.0). Up to five shards every shard
167
- flushes every 2 seconds; eight shards every 3.2; sixteen every 6.4; from 75
168
- shards the cap holds it at 30.
169
-
170
- Only the timer scales. All shards of a run share one rate budget on Squally's
171
- side (300 requests a minute per key), and the timer keeps all of them
172
- together at about half of it. The 25-result threshold is left alone
173
- deliberately: it bounds how long a *fast* suite waits, and stretching it would
174
- delay results without bounding the burst any better.
175
-
176
- A batch also stays under 3 MB (0.12.0): results with long error texts or many
177
- steps simply go out in more, smaller requests.
178
-
179
- There is no longer a single-request size limit that could discard a whole run -
180
- a very large suite simply takes more, smaller requests. As a courtesy the
181
- reporter still warns if a run's result count climbs well past 5,000, since that
182
- usually means retries are configured higher than intended rather than a suite
183
- that's actually that large.
184
-
185
- If Squally is unreachable partway through, results already sent stay in the
186
- dashboard; only what could not be delivered is lost, and your test run's exit
187
- code is never affected either way.
188
-
189
- ## Steps (0.12.0)
190
-
191
- For an attempt that **failed, or needed a retry**, the reporter also sends the
192
- steps Playwright recorded - hooks, fixtures, actions, assertions and your own
193
- `test.step`s, with their durations and `file:line`. Squally shows them on the
194
- attempt page, the failing step marked. An attempt that passed on its first try
195
- sends none.
196
-
197
- - **Typed values never leave your runner.** The value of a `fill`, `type` or
198
- `pressSequentially` is replaced: `Fill "•••" getByLabel('Password')`. Every
199
- title also goes through the same secret masking as error texts.
200
- - **At most 300 steps per attempt, 200 characters per title.** A longer list
201
- keeps the failing step and the steps it sits in, then the rest in order, and
202
- tells Squally how many were left out.
203
- - Nothing to configure: steps come from Playwright's own report, with or
204
- without tracing.
205
-
206
- **Only actual test runs appear.** A run shows up in Squally once your first
207
- test starts, so commands that never run a test create nothing: `--list`,
208
- a `globalSetup` or `webServer` that fails to start, a `--grep` that matches
209
- nothing. A run that *does* execute and legitimately contains no tests — an
210
- empty shard, `--only-changed` with no changed test file, `--pass-with-no-tests`
211
- — is still reported, as a run with zero results.
212
-
213
- **Timeouts and interrupted tests.** A test that runs out of time is reported
214
- as failed, and Squally is told that it timed out and what its timeout was -
215
- including a `test.setTimeout()` or `test.slow()` from inside the test. A test
216
- that Playwright cuts off because the whole run is stopping (`maxFailures`
217
- reached, Ctrl+C) is reported as skipped: it did not fail, and it does not
218
- count against the test.
219
-
220
- ## Cancelled runs
221
-
222
- **Cancelled in Squally.** An admin can cancel a running run from its page in
223
- Squally. That marks the run there; it does not stop your CI job. What the job
224
- sends afterwards is discarded, and from 0.12.0 the reporter says so in one
225
- line - "This run was cancelled in Squally" - instead of advice about job keys.
226
-
227
- A run stopped with SIGINT - Ctrl+C, or a cancelled CI job - shows in Squally
228
- as **Cancelled**: the reporter sends what finished and tells Squally the run
229
- was interrupted. Your exit code stays Playwright's own (130). This needs
230
- Playwright itself to receive the signal.
231
-
232
- **On GitHub Actions, start Playwright with `exec`.** A cancelled workflow
233
- sends SIGINT only to the step's top-level process. With
234
- `run: npx playwright test` that process is the shell, so Playwright never sees
235
- the signal and is killed outright when GitHub stops waiting. `exec` makes
236
- Playwright the top-level process:
237
-
238
- ```yaml
239
- - name: Run Playwright tests
240
- run: exec ./node_modules/.bin/playwright test
241
- ```
242
-
243
- This needs a bash step (`shell: bash`, the default on Linux and macOS
244
- runners). Other CI systems deliver a cancel in their own way - check that
245
- Playwright is the process that receives it.
246
-
247
- Without that - or after any hard kill (SIGKILL) - the reporter never gets to
248
- report the end: the run stays *running* until Squally's hourly check marks it
249
- **incomplete** once the project's run timeout (60 minutes by default) has
250
- passed. A run stopped before its first test starts sends nothing, like any
251
- run that never ran a test, and `globalTimeout` running out is not a cancel -
252
- that run is simply failed.
253
-
254
- ## Which CI, branch and commit a run is reported under
255
-
256
- Squally counts **CI runs** in its test metrics, so the reporter works out
257
- which CI system it is running on from that system's own environment:
258
-
259
- | Provider | Recognised by | Shards grouped by | Branch | Commit |
260
- |---|---|---|---|---|
261
- | GitHub Actions | `GITHUB_ACTIONS` | `GITHUB_RUN_ID`-`GITHUB_RUN_ATTEMPT` | `GITHUB_REF_NAME`; pull requests: `GITHUB_HEAD_REF` | `GITHUB_SHA`; pull requests: the PR's head commit |
262
- | GitLab CI | `GITLAB_CI` | `CI_PIPELINE_ID` | `CI_COMMIT_BRANCH`; merge requests: `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` | `CI_COMMIT_SHA`; merged-results pipelines: `CI_MERGE_REQUEST_SOURCE_BRANCH_SHA` |
263
- | CircleCI | `CIRCLECI` | `CIRCLE_WORKFLOW_ID` | `CIRCLE_BRANCH` | `CIRCLE_SHA1` |
264
- | Jenkins | `BUILD_TAG` (`jenkins-…`) | `BUILD_TAG` | `CHANGE_BRANCH`, else `BRANCH_NAME`, else `GIT_BRANCH` | `GIT_COMMIT` |
265
- | Azure Pipelines | `TF_BUILD` | `BUILD_BUILDID` | `BUILD_SOURCEBRANCH`; pull requests: `SYSTEM_PULLREQUEST_SOURCEBRANCH` | `BUILD_SOURCEVERSION`; pull requests: `SYSTEM_PULLREQUEST_SOURCECOMMITID` |
266
- | Bitbucket Pipelines | `BITBUCKET_BUILD_NUMBER` | `BITBUCKET_BUILD_NUMBER` | `BITBUCKET_BRANCH` | `BITBUCKET_COMMIT` |
267
- | Buildkite | `BUILDKITE` | `BUILDKITE_BUILD_ID` | `BUILDKITE_BRANCH` | `BUILDKITE_COMMIT` |
268
- | TeamCity | `TEAMCITY_VERSION` | - (set `SQUALLY_CI_RUN_ID`) | from git | `BUILD_VCS_NUMBER` |
269
- | AWS CodeBuild | `CODEBUILD_BUILD_ID` | `CODEBUILD_BUILD_ID` | `CODEBUILD_WEBHOOK_HEAD_REF` | `CODEBUILD_RESOLVED_SOURCE_VERSION` |
270
- | Travis CI | `TRAVIS` | `TRAVIS_BUILD_ID` | `TRAVIS_BRANCH`; pull requests: `TRAVIS_PULL_REQUEST_BRANCH` | `TRAVIS_COMMIT`; pull requests: `TRAVIS_PULL_REQUEST_SHA` |
271
- | Drone | `DRONE` | `DRONE_BUILD_NUMBER` | `DRONE_BRANCH`; pull requests: `DRONE_SOURCE_BRANCH` | `DRONE_COMMIT_SHA` |
272
- | any other CI | `CI=true` or `CI=1` | - (set `SQUALLY_CI_RUN_ID`) | from git | from git |
273
-
274
- - **`CI` counts only as `true` or `1`.** `CI=false` or `CI=0` is a local run -
275
- unless one of the providers above is recognised, which always wins. A run
276
- that is none of these is local. Local runs are listed with all other runs,
277
- and have numbers of their own: Squally's figures count CI runs by default,
278
- and the **All · CI · Local** switch shows local runs on their own, or both.
279
- A local run never posts a GitHub check, a pull-request comment or a Slack
280
- message.
281
- - **Pull requests are reported under their own head commit and source
282
- branch**, not the merge commit CI checks out - that is the commit Squally's
283
- GitHub check and pull-request comment have to find. The commit message and
284
- author are that commit's too - never the "Merge … into …" message of the
285
- checkout.
286
- - **The head commit is fetched when the checkout lacks it.** With
287
- `actions/checkout`'s default `fetch-depth: 1` the head commit is not in the
288
- clone, so the reporter fetches that one commit from `origin`
289
- (`git fetch --depth=1 --no-tags`), waits at most 3 seconds, and reads its
290
- message and author. It only does so in a shallow checkout - fetching into a
291
- full clone would make it shallow for the rest of your job - and never
292
- changes `HEAD` or any branch. If the fetch fails or takes longer, the
293
- message and author stay empty and the reporter says so once; the run and
294
- its exit code are unaffected. Alternatives: `fetch-depth: 2` on the checkout
295
- step puts the head commit in the clone, so nothing is fetched, and
296
- `SQUALLY_DISABLE_HEAD_COMMIT_FETCH=true` turns the fetch off.
297
- - **A tag build has no branch.** A detached checkout never reports `HEAD` as a
298
- branch name, and `refs/heads/` or `origin/` prefixes are removed.
299
- - **A link to the CI run (0.11.0).** The reporter sends the page of the CI run,
300
- so Squally can link a run to it. It is read from each provider's own
301
- variable, sent only as an `https://` URL of at most 2,048 characters, and
302
- never opened by Squally - only shown as a link:
303
-
304
- | Provider | Link |
305
- |---|---|
306
- | GitHub Actions | `GITHUB_SERVER_URL`/`GITHUB_REPOSITORY`/actions/runs/`GITHUB_RUN_ID`/attempts/`GITHUB_RUN_ATTEMPT` |
307
- | GitLab CI | `CI_PIPELINE_URL` |
308
- | Jenkins | `BUILD_URL` (only with Jenkins served over https) |
309
- | Buildkite | `BUILDKITE_BUILD_URL` |
310
- | Travis CI | `TRAVIS_BUILD_WEB_URL` |
311
- | Drone | `DRONE_BUILD_LINK` |
312
-
313
- Other providers send no link. It is the run's page, never one job's, and it
314
- is the same with `SQUALLY_CI_RUN_ID` set.
315
-
316
- ## Tests marked with `test.fail()`
317
-
318
- Playwright's `test.fail()` marks a test as *expected to fail*: it guards a bug
319
- you already know about, and Playwright runs the test anyway to check that it
320
- does still fail. The interesting moment is the opposite one - when someone
321
- fixes the bug, the test starts passing and Playwright turns it red so you
322
- remember to delete the marker.
323
-
324
- The reporter passes that marker on, so Squally reads such a test the same way
325
- Playwright does:
326
-
327
- - **Marked and still failing** - shown as passing, with an "expected failure"
328
- label. It counts as a pass in the test's stability, not against its failure
329
- rate, and it does not fail Squally's pull-request check.
330
- - **Marked and now passing** - shown as a failure, because the marker is out of
331
- date and should be removed.
332
- - **Marked and timed out** - shown as a failure. Playwright does not count a
333
- timeout as the expected failure either, so the marker is not passed on for
334
- it.
335
-
336
- Both ways of marking a test are recognised: the `test.fail()` modifier and a
337
- `testInfo.fail()` call inside the test body. `test.skip()` and `test.fixme()`
338
- are a different statement and are not affected.
339
-
340
- Nothing is reported for an unmarked test, so an ordinary suite is unchanged.
341
- Note also that Playwright itself treats a marked, failing test as expected, so
342
- with `screenshot: "only-on-failure"` it captures nothing for it - there is no
343
- artifact for the reporter to upload, which is consistent rather than missing.
344
-
345
- ## Sharding
346
-
347
- **Handled automatically - no extra setup needed.** Each shard is still a
348
- separate process with its own reporter instance, but Squally now correlates
349
- them itself: all shards of one CI run are grouped together and shown as one
350
- run once every shard has finished, with the flip side of that guarantee being
351
- that the run only completes once every shard *has* reported.
352
-
353
- Correlation is based on an id read from your CI provider's own environment
354
- (the "Shards grouped by" column above), so it costs you nothing on every
355
- provider in that table except TeamCity. On TeamCity, on a CI system not in the
356
- table, or when you shard locally across several separate `--shard`
357
- invocations, set `SQUALLY_CI_RUN_ID` to the same value for every shard of one
358
- run - anything unique per CI run and shared by its shards works, e.g. a build
359
- number your CI system already exposes.
360
-
361
- **Several jobs in one CI run need their own key (0.11.0).** The id is shared
362
- by everything in one CI run, so two jobs that are *not* shards of each other -
363
- a GitHub Actions matrix over browsers run without `--shard`, or an `api` and a
364
- `ui` suite - would report into the same Squally run. Give each job its own
365
- `SQUALLY_CI_JOB_KEY`, shared only by that job's own shards:
366
-
367
- ```yaml
368
- jobs:
369
- test:
370
- strategy:
371
- matrix:
372
- browser: [chromium, firefox]
373
- env:
374
- SQUALLY_CI_JOB_KEY: ${{ matrix.browser }}
375
- steps:
376
- - run: exec ./node_modules/.bin/playwright test --project=${{ matrix.browser }}
377
- ```
378
-
379
- The reporter puts the key into the id - `<GITHUB_RUN_ID>-<key>-<GITHUB_RUN_ATTEMPT>`
380
- on GitHub Actions, `<provider id>-<key>` elsewhere - so each job is its own run
381
- in Squally, and a re-run of one job links to that job's own earlier attempt.
382
- The key may hold letters, digits, `.`, `_` and `-`, at most 64 characters; an
383
- invalid key is reported once and ignored, and your test run is unaffected.
384
- Without a key the id is exactly what it was before 0.11.0. A suite that is
385
- sharded *and* split into jobs uses the same key for all shards of one job:
386
- `SQUALLY_CI_JOB_KEY: ${{ matrix.suite }}` beside `--shard=${{ matrix.shard }}/2`.
387
-
388
- **Replaces the hand-built id.** Until 0.11.0 this section suggested
389
- `SQUALLY_CI_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.browser }}`.
390
- It still groups each job correctly, but Squally cannot tell its attempts apart,
391
- so a re-run is never linked to the attempt before it. Switch to
392
- `SQUALLY_CI_JOB_KEY`. An explicit `SQUALLY_CI_RUN_ID` still wins for the id;
393
- the key is then only sent alongside it.
394
-
395
- **When two jobs collide anyway, Squally refuses the second one at the start**
396
- (squally-app with block 1b, reporter 0.11.0). The reporter prints one block,
397
- sends nothing more for that job, and leaves your test run and its exit code
398
- alone:
399
-
400
- - `ci_run_collision` - another job already reports under this CI run id (and
401
- shard). Give every job its own `SQUALLY_CI_JOB_KEY`.
402
- - `ci_run_mixed` - the id would put a sharded job and an unsharded one, or
403
- shards of different totals, into one run. The same fix.
404
-
405
- Before 0.11.0 the second job was handed the first job's run instead, and its
406
- results were refused once that run completed; a reporter older than 0.11.0
407
- still behaves that way, and prints "Squally has already completed run …".
408
-
409
- **Re-runs.** On GitHub Actions every re-run attempt is its own run in Squally,
410
- linked to the attempt before it ("Re-run of #N"). "Re-run failed jobs" runs only
411
- the shards that did not pass; that attempt waits only for those shards,
412
- completes once they have reported, and passes or fails on the tests they ran -
413
- the reused shards are neither waited for nor counted, and the run page shows
414
- them as "passed in #N · not re-run". A re-run shard that never reports still
415
- leaves the run incomplete after the run timeout.
416
-
417
- Other CI systems re-run a job under the id of the run it belongs to - a GitLab
418
- job retry, an Azure Pipelines re-run, a Bitbucket re-run of failed steps, a
419
- Buildkite retry, a Travis CI restart. Since 0.11.0 Squally refuses such a
420
- retry at the start with `ci_run_collision`: the slot is taken by the first
421
- attempt. **`SQUALLY_CI_JOB_KEY` does not help here** - the retry is the same
422
- job - so a retry on these systems is not reported; the first attempt stays the
423
- run. A Drone restart is a new build with a new number, and so a new run in
424
- Squally, not linked to the one it repeats.
425
-
426
- **`playwright merge-reports` is not part of this and is not recommended** for
427
- getting a single run in Squally - the automatic correlation above is the
428
- supported path, and produces correct per-shard timing/branch/commit
429
- information that a merge job cannot (see the historical note below). Nothing
430
- stops you from using `merge-reports` for Playwright's own HTML report if you
431
- want that separately; just don't point this reporter at the merged result
432
- expecting Squally to treat it specially.
433
-
434
- <details>
435
- <summary>Why not just use `merge-reports`, historically</summary>
436
-
437
- An earlier version of this reporter recommended `playwright merge-reports` as
438
- the way to get one Squally run out of a sharded job. That path has a real
439
- problem: branch, commit, commit message and author are all read on whichever
440
- machine the reporter actually runs on - during a merge that is the merge job,
441
- not the shards - and without a checkout there (and without the CI provider's
442
- own commit variables) they all come out empty. A run with no branch is never
443
- counted as a run on your stable branch - the branch the dashboard measures by
444
- default - and no branch filter finds it, so a "merged" run could exist in the
445
- dashboard while missing from everything Squally measures per branch: suite
446
- stability, and each test's stability, flaky rate and failure rate on that
447
- branch. The automatic correlation above reads that information from each
448
- shard's own environment instead, so this problem does not arise.
449
-
450
- </details>
451
-
452
- ## Tested revision (optional)
453
-
454
- The tested revision is the state of your **application** a run tested, as
455
- opposed to the commit the run is reported under. Squally does not know it by
456
- itself - it is **opt-in**, with one of these two:
457
-
458
- ```bash
459
- # The tests live in the same repo as the application: the commit this CI job
460
- # checked out is the tested revision.
461
- SAME_REPO_AS_APP=true
462
-
463
- # The tests live in a separate repo (e.g. a dedicated E2E repo pointed at a
464
- # deployed app): pass the tested revision itself - a commit SHA, tag, or
465
- # branch name/ref.
466
- TESTED_REVISION=v1.2.0
467
- ```
468
-
469
- - **`SAME_REPO_AS_APP=true`** sends the commit your job checked out: your CI
470
- provider's own variable for it (`GITHUB_SHA`, `CI_COMMIT_SHA`,
471
- `CIRCLE_SHA1`, `GIT_COMMIT`, `BUILD_SOURCEVERSION`, `BITBUCKET_COMMIT`,
472
- `BUILDKITE_COMMIT`, `BUILD_VCS_NUMBER`, `CODEBUILD_RESOLVED_SOURCE_VERSION`,
473
- `TRAVIS_COMMIT`, `DRONE_COMMIT_SHA`), or `git rev-parse HEAD` where there is
474
- none - including local runs. Only a commit SHA is sent; if neither yields
475
- one, the run has no tested revision.
476
- - **On a pull request the two commits differ, on purpose.** The run is
477
- reported under the pull request's head commit (see the provider table
478
- above) - that is what Squally's check and PR comment need. The tested
479
- revision is the merge commit CI checked out, because that is what the tests
480
- ran against. On a Bitbucket pull request and a GitHub `pull_request_target`
481
- run it comes from `git rev-parse HEAD`.
482
- - **`TESTED_REVISION` wins** when both are set.
483
- - Neither variable is required - without them, test results are still sent
484
- and stored exactly as before.
485
-
486
- **The value is normalised before it is stored**, so that the same revision
487
- written two ways still compares equal: it is lowercased, and a leading `v` or a
488
- `refs/heads/` prefix is removed. The example above is exactly such a case -
489
- `v1.2.0` is stored as `1.2.0`. The reporter says so once per run when it
490
- happens, so a value that looks different in the dashboard is not a mystery.
491
-
492
- ## Troubleshooting
493
-
494
- Set `SQUALLY_DEBUG=1` to print detail that is otherwise kept quiet - the
495
- browser-console extraction (how many messages were found, or that an attempt's
496
- trace held none) and, since 0.11.0, a CI run link that was not sent because it
497
- was not an `https://` URL. Everything that is *not* ordinary (a trace that
498
- cannot be read, an unsupported schema version, a rejected upload) is printed
499
- without it.
500
-
501
- ## Support
502
-
503
- Found a bug or have a question? Please open an issue on
504
- [GitHub Issues](https://github.com/squallydev-admin/squally-reporter/issues) -
505
- that's the primary way to report problems or ask questions.
506
-
507
- ## License
508
-
509
- MIT - see [LICENSE](./LICENSE).
1
+ # squally-reporter
2
+
3
+ Playwright reporter that sends test results to your Squally dashboard.
4
+
5
+ Learn more at [app.squally.dev](https://app.squally.dev).
6
+
7
+ ## Requirements
8
+
9
+ - Node.js >= 20.18.1
10
+ - `@playwright/test` >= 1.30.0
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install -D squally-reporter
16
+ ```
17
+
18
+ ## Config
19
+
20
+ Set one env var (e.g. as a CI secret):
21
+
22
+ ```bash
23
+ SQUALLY_INGEST_KEY=your-project-ingest-key
24
+ ```
25
+
26
+ Find your ingest key in the Squally dashboard under **Settings › Project ›
27
+ Ingest key**.
28
+
29
+ **Since 0.12.0 the URL defaults to `https://app.squally.dev`.** Set
30
+ `SQUALLY_INGEST_URL` only to report to another Squally. Without a key the
31
+ reporter sends nothing and says so once.
32
+
33
+ **Renamed in 0.9.0.** These were `SQUALLY_API_URL` and `SQUALLY_API_KEY`. The
34
+ old names still work for now, with a deprecation warning in the CI log - but
35
+ they belong to Squally's read API now, so rename them. `SQUALLY_API_URL` is
36
+ only used together with `SQUALLY_API_KEY`, never on its own - and when it is
37
+ set but not used, the reporter sends nothing rather than falling back to the
38
+ default URL, since the variable says you meant another target.
39
+
40
+ **A read key is never sent.** A key starting with `sqly_ro_` is a read API key,
41
+ for scripts and agents; reporting needs the ingest key. Since 0.12.0 the
42
+ reporter refuses one in any of `SQUALLY_INGEST_KEY`, `SQUALLY_API_KEY` or the
43
+ `apiKey` option before sending anything, and says which one to fix.
44
+
45
+ Then add the reporter to `playwright.config.ts`:
46
+
47
+ ```ts
48
+ import { defineConfig } from "@playwright/test";
49
+
50
+ export default defineConfig({
51
+ reporter: [
52
+ ["list"],
53
+ ["squally-reporter"],
54
+ ],
55
+ });
56
+ ```
57
+
58
+ Alternatively, pass config explicitly instead of using env vars - but **prefer
59
+ the env vars above for `apiKey`, and not only because a literal in a committed
60
+ file is obviously wrong.** Playwright's `json` and `blob` reporters serialize
61
+ your resolved config into their output file, options and all: an `apiKey`
62
+ passed here ends up in that file in plain text, even when its value came from
63
+ an environment variable at runtime. What gets written is the option, not where
64
+ it came from. The reporter warns, naming the file, when it sees this
65
+ combination. Measured against a real report, 20.09.2026.
66
+
67
+ ```ts
68
+ reporter: [
69
+ ["squally-reporter", { apiUrl: "https://app.squally.dev", apiKey: process.env.SQUALLY_INGEST_KEY }],
70
+ ],
71
+ ```
72
+
73
+ That's it - run your tests as usual (`npx playwright test`); results stream
74
+ to Squally while the suite runs (see *How results reach Squally* below).
75
+
76
+ ## Artifact upload (screenshot, trace, browser console)
77
+
78
+ If your `playwright.config.ts` captures screenshots or traces, the reporter
79
+ uploads them to Squally so they show up next to the failing test in the
80
+ dashboard - no separate setup beyond the two env vars above.
81
+
82
+ Three kinds are uploaded:
83
+
84
+ | Kind | Where it comes from |
85
+ | --- | --- |
86
+ | **Screenshot** | the file Playwright writes for a failed attempt |
87
+ | **Trace** | the trace zip Playwright writes |
88
+ | **Browser console** | read out of that trace zip by this reporter |
89
+
90
+ **The browser console needs a trace.** It is not something Playwright hands a
91
+ reporter: `result.stdout`/`stderr` are the output of the *Node* process - what
92
+ your test file printed - while what the *page* printed only exists inside the
93
+ trace. So the reporter opens the trace and reads it out, which means an
94
+ attempt with no trace has no browser console, and nothing can change that but
95
+ turning tracing on. Squally's attempt page says which of the two it is rather
96
+ than showing an empty section.
97
+
98
+ Supported trace schema versions: **5 to 10**, which covers Playwright 1.30
99
+ through 1.62 (1.62 writes version 8). A trace outside that range is left alone
100
+ and the reporter prints one warning naming the version - it never uploads an
101
+ empty console file, because "no messages" and "this reporter could not read
102
+ it" must not look the same in the dashboard.
103
+
104
+ **Only from attempts worth looking at:** artifacts are uploaded only for
105
+ attempts that **failed**, or that **needed a retry** (even if that retry then
106
+ passed) - never for an attempt that passed on its first try, regardless of how
107
+ `screenshot`/`trace` are configured. Nobody looks at a screenshot of a test
108
+ that just passed, and uploading one anyway would only eat into your
109
+ organization's storage for no benefit.
110
+
111
+ **You need to actually enable capture in `playwright.config.ts`** - without
112
+ it, Playwright doesn't produce any attachments, so there's nothing for the
113
+ reporter to find or upload (no error either way, it's a silent no-op):
114
+
115
+ ```ts
116
+ export default defineConfig({
117
+ use: {
118
+ screenshot: "only-on-failure",
119
+ trace: "retain-on-failure",
120
+ },
121
+ // ...
122
+ });
123
+ ```
124
+
125
+ **Limits.** None of these are enforced by the reporter - Squally applies them
126
+ when the upload is requested and answers with a reason, which is what the
127
+ reporter prints. The numbers below are therefore Squally's current ones rather
128
+ than anything this package guarantees, and can change server-side without a new
129
+ release of it:
130
+
131
+ - **50 MB per screenshot or trace, 4 MB per browser console**, the same on
132
+ every plan. A larger file is rejected before any bytes are uploaded; the
133
+ reporter logs a `console.warn` naming the file and its limit and moves on.
134
+ The test result itself is reported as usual, and the run's outcome and exit
135
+ code are untouched. In the dashboard that artifact is shown as skipped, with
136
+ the reason, rather than silently missing.
137
+ - **At most 250 artifacts per shard.** Squally keeps the first 250 and skips
138
+ the rest with the reason `limit`, which the reporter explains in one line.
139
+ - **A total storage cap per organization, depending on your Squally plan.**
140
+ Once it is reached, further uploads are rejected the same way until you free
141
+ up space or upgrade. Note that a fully instrumented suite now produces up to
142
+ three artifacts per failed attempt rather than two.
143
+ - The browser console is a text file of a few KB - it is extracted from the
144
+ trace you are already uploading, so it adds no capture cost to your run.
145
+ - **Your test's own `stdout`/`stderr` is cut at 16 KB** before it is sent. That
146
+ is this reporter's own limit, not Squally's, and it applies whether or not
147
+ you capture artifacts at all.
148
+ - Two minutes per upload, and 30 seconds for the calls that surround it. An
149
+ unreachable or very slow server means a skipped upload and a `console.warn`,
150
+ not a CI job waiting indefinitely.
151
+
152
+ Upload is best-effort and fully non-blocking, same principle as the rest of
153
+ this reporter - a failed or skipped upload never fails your test run or
154
+ changes its exit code, it only logs a `console.warn` explaining what
155
+ happened.
156
+
157
+ ## How results reach Squally
158
+
159
+ Results stream to Squally as your suite runs, not all at once at the end:
160
+ each test's result is sent in a small batch, so the "running" view in the
161
+ dashboard stays close to real time even for a long suite. A batch goes out on
162
+ whichever of these comes first:
163
+
164
+ - **25 results.** Fixed, and the same whether you shard or not.
165
+ - **A timer: 2 seconds, or 0.4 seconds × the number of shards when that is
166
+ longer, capped at 30 seconds** (0.12.0). Up to five shards every shard
167
+ flushes every 2 seconds; eight shards every 3.2; sixteen every 6.4; from 75
168
+ shards the cap holds it at 30.
169
+
170
+ Only the timer scales. All shards of a run share one rate budget on Squally's
171
+ side (300 requests a minute per key), and the timer keeps all of them
172
+ together at about half of it. The 25-result threshold is left alone
173
+ deliberately: it bounds how long a *fast* suite waits, and stretching it would
174
+ delay results without bounding the burst any better.
175
+
176
+ A batch also stays under 3 MB (0.12.0): results with long error texts or many
177
+ steps simply go out in more, smaller requests.
178
+
179
+ There is no longer a single-request size limit that could discard a whole run -
180
+ a very large suite simply takes more, smaller requests. As a courtesy the
181
+ reporter still warns if a run's result count climbs well past 5,000, since that
182
+ usually means retries are configured higher than intended rather than a suite
183
+ that's actually that large.
184
+
185
+ If Squally is unreachable partway through, results already sent stay in the
186
+ dashboard; only what could not be delivered is lost, and your test run's exit
187
+ code is never affected either way.
188
+
189
+ ## Steps (0.12.0)
190
+
191
+ For an attempt that **failed, or needed a retry**, the reporter also sends the
192
+ steps Playwright recorded - hooks, fixtures, actions, assertions and your own
193
+ `test.step`s, with their durations and `file:line`. Squally shows them on the
194
+ attempt page, the failing step marked. An attempt that passed on its first try
195
+ sends none.
196
+
197
+ - **Typed values never leave your runner.** The value of a `fill`, `type` or
198
+ `pressSequentially` is replaced: `Fill "•••" getByLabel('Password')`. Every
199
+ title also goes through the same secret masking as error texts.
200
+ - **At most 300 steps per attempt, 200 characters per title.** A longer list
201
+ keeps the failing step and the steps it sits in, then the rest in order, and
202
+ tells Squally how many were left out.
203
+ - Nothing to configure: steps come from Playwright's own report, with or
204
+ without tracing.
205
+
206
+ **Only actual test runs appear.** A run shows up in Squally once your first
207
+ test starts, so commands that never run a test create nothing: `--list`,
208
+ a `globalSetup` or `webServer` that fails to start, a `--grep` that matches
209
+ nothing. A run that *does* execute and legitimately contains no tests — an
210
+ empty shard, `--only-changed` with no changed test file, `--pass-with-no-tests`
211
+ — is still reported, as a run with zero results.
212
+
213
+ **Timeouts and interrupted tests.** A test that runs out of time is reported
214
+ as failed, and Squally is told that it timed out and what its timeout was -
215
+ including a `test.setTimeout()` or `test.slow()` from inside the test. A test
216
+ that Playwright cuts off because the whole run is stopping (`maxFailures`
217
+ reached, Ctrl+C) is reported as skipped: it did not fail, and it does not
218
+ count against the test.
219
+
220
+ ## Cancelled runs
221
+
222
+ **Cancelled in Squally.** An admin can cancel a running run from its page in
223
+ Squally. That marks the run there; it does not stop your CI job. What the job
224
+ sends afterwards is discarded, and from 0.12.0 the reporter says so in one
225
+ line - "This run was cancelled in Squally" - instead of advice about job keys.
226
+
227
+ A run stopped with SIGINT - Ctrl+C, or a cancelled CI job - shows in Squally
228
+ as **Cancelled**: the reporter sends what finished and tells Squally the run
229
+ was interrupted. Your exit code stays Playwright's own (130). This needs
230
+ Playwright itself to receive the signal.
231
+
232
+ **On GitHub Actions**, run Playwright as usual:
233
+
234
+ ```yaml
235
+ - name: Run Playwright tests
236
+ run: npx playwright test
237
+ ```
238
+
239
+ A cancelled workflow currently shows as incomplete in Squally.
240
+
241
+ Without that - or after any hard kill (SIGKILL) - the reporter never gets to
242
+ report the end: the run stays *running* until Squally's hourly check marks it
243
+ **incomplete** once the project's run timeout (60 minutes by default) has
244
+ passed. A run stopped before its first test starts sends nothing, like any
245
+ run that never ran a test, and `globalTimeout` running out is not a cancel -
246
+ that run is simply failed.
247
+
248
+ ## Which CI, branch and commit a run is reported under
249
+
250
+ Squally counts **CI runs** in its test metrics, so the reporter works out
251
+ which CI system it is running on from that system's own environment:
252
+
253
+ | Provider | Recognised by | Shards grouped by | Branch | Commit |
254
+ |---|---|---|---|---|
255
+ | GitHub Actions | `GITHUB_ACTIONS` | `GITHUB_RUN_ID`-`GITHUB_RUN_ATTEMPT` | `GITHUB_REF_NAME`; pull requests: `GITHUB_HEAD_REF` | `GITHUB_SHA`; pull requests: the PR's head commit |
256
+ | GitLab CI | `GITLAB_CI` | `CI_PIPELINE_ID` | `CI_COMMIT_BRANCH`; merge requests: `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME` | `CI_COMMIT_SHA`; merged-results pipelines: `CI_MERGE_REQUEST_SOURCE_BRANCH_SHA` |
257
+ | CircleCI | `CIRCLECI` | `CIRCLE_WORKFLOW_ID` | `CIRCLE_BRANCH` | `CIRCLE_SHA1` |
258
+ | Jenkins | `BUILD_TAG` (`jenkins-…`) | `BUILD_TAG` | `CHANGE_BRANCH`, else `BRANCH_NAME`, else `GIT_BRANCH` | `GIT_COMMIT` |
259
+ | Azure Pipelines | `TF_BUILD` | `BUILD_BUILDID` | `BUILD_SOURCEBRANCH`; pull requests: `SYSTEM_PULLREQUEST_SOURCEBRANCH` | `BUILD_SOURCEVERSION`; pull requests: `SYSTEM_PULLREQUEST_SOURCECOMMITID` |
260
+ | Bitbucket Pipelines | `BITBUCKET_BUILD_NUMBER` | `BITBUCKET_BUILD_NUMBER` | `BITBUCKET_BRANCH` | `BITBUCKET_COMMIT` |
261
+ | Buildkite | `BUILDKITE` | `BUILDKITE_BUILD_ID` | `BUILDKITE_BRANCH` | `BUILDKITE_COMMIT` |
262
+ | TeamCity | `TEAMCITY_VERSION` | - (set `SQUALLY_CI_RUN_ID`) | from git | `BUILD_VCS_NUMBER` |
263
+ | AWS CodeBuild | `CODEBUILD_BUILD_ID` | `CODEBUILD_BUILD_ID` | `CODEBUILD_WEBHOOK_HEAD_REF` | `CODEBUILD_RESOLVED_SOURCE_VERSION` |
264
+ | Travis CI | `TRAVIS` | `TRAVIS_BUILD_ID` | `TRAVIS_BRANCH`; pull requests: `TRAVIS_PULL_REQUEST_BRANCH` | `TRAVIS_COMMIT`; pull requests: `TRAVIS_PULL_REQUEST_SHA` |
265
+ | Drone | `DRONE` | `DRONE_BUILD_NUMBER` | `DRONE_BRANCH`; pull requests: `DRONE_SOURCE_BRANCH` | `DRONE_COMMIT_SHA` |
266
+ | any other CI | `CI=true` or `CI=1` | - (set `SQUALLY_CI_RUN_ID`) | from git | from git |
267
+
268
+ - **`CI` counts only as `true` or `1`.** `CI=false` or `CI=0` is a local run -
269
+ unless one of the providers above is recognised, which always wins. A run
270
+ that is none of these is local. Local runs are listed with all other runs,
271
+ and have numbers of their own: Squally's figures count CI runs by default,
272
+ and the **All · CI · Local** switch shows local runs on their own, or both.
273
+ A local run never posts a GitHub check, a pull-request comment or a Slack
274
+ message.
275
+ - **Pull requests are reported under their own head commit and source
276
+ branch**, not the merge commit CI checks out - that is the commit Squally's
277
+ GitHub check and pull-request comment have to find. The commit message and
278
+ author are that commit's too - never the "Merge … into …" message of the
279
+ checkout.
280
+ - **The head commit is fetched when the checkout lacks it.** With
281
+ `actions/checkout`'s default `fetch-depth: 1` the head commit is not in the
282
+ clone, so the reporter fetches that one commit from `origin`
283
+ (`git fetch --depth=1 --no-tags`), waits at most 3 seconds, and reads its
284
+ message and author. It only does so in a shallow checkout - fetching into a
285
+ full clone would make it shallow for the rest of your job - and never
286
+ changes `HEAD` or any branch. If the fetch fails or takes longer, the
287
+ message and author stay empty and the reporter says so once; the run and
288
+ its exit code are unaffected. Alternatives: `fetch-depth: 2` on the checkout
289
+ step puts the head commit in the clone, so nothing is fetched, and
290
+ `SQUALLY_DISABLE_HEAD_COMMIT_FETCH=true` turns the fetch off.
291
+ - **A tag build has no branch.** A detached checkout never reports `HEAD` as a
292
+ branch name, and `refs/heads/` or `origin/` prefixes are removed.
293
+ - **A link to the CI run (0.11.0).** The reporter sends the page of the CI run,
294
+ so Squally can link a run to it. It is read from each provider's own
295
+ variable, sent only as an `https://` URL of at most 2,048 characters, and
296
+ never opened by Squally - only shown as a link:
297
+
298
+ | Provider | Link |
299
+ |---|---|
300
+ | GitHub Actions | `GITHUB_SERVER_URL`/`GITHUB_REPOSITORY`/actions/runs/`GITHUB_RUN_ID`/attempts/`GITHUB_RUN_ATTEMPT` |
301
+ | GitLab CI | `CI_PIPELINE_URL` |
302
+ | Jenkins | `BUILD_URL` (only with Jenkins served over https) |
303
+ | Buildkite | `BUILDKITE_BUILD_URL` |
304
+ | Travis CI | `TRAVIS_BUILD_WEB_URL` |
305
+ | Drone | `DRONE_BUILD_LINK` |
306
+
307
+ Other providers send no link. It is the run's page, never one job's, and it
308
+ is the same with `SQUALLY_CI_RUN_ID` set.
309
+
310
+ ## Tests marked with `test.fail()`
311
+
312
+ Playwright's `test.fail()` marks a test as *expected to fail*: it guards a bug
313
+ you already know about, and Playwright runs the test anyway to check that it
314
+ does still fail. The interesting moment is the opposite one - when someone
315
+ fixes the bug, the test starts passing and Playwright turns it red so you
316
+ remember to delete the marker.
317
+
318
+ The reporter passes that marker on, so Squally reads such a test the same way
319
+ Playwright does:
320
+
321
+ - **Marked and still failing** - shown as passing, with an "expected failure"
322
+ label. It counts as a pass in the test's stability, not against its failure
323
+ rate, and it does not fail Squally's pull-request check.
324
+ - **Marked and now passing** - shown as a failure, because the marker is out of
325
+ date and should be removed.
326
+ - **Marked and timed out** - shown as a failure. Playwright does not count a
327
+ timeout as the expected failure either, so the marker is not passed on for
328
+ it.
329
+
330
+ Both ways of marking a test are recognised: the `test.fail()` modifier and a
331
+ `testInfo.fail()` call inside the test body. `test.skip()` and `test.fixme()`
332
+ are a different statement and are not affected.
333
+
334
+ Nothing is reported for an unmarked test, so an ordinary suite is unchanged.
335
+ Note also that Playwright itself treats a marked, failing test as expected, so
336
+ with `screenshot: "only-on-failure"` it captures nothing for it - there is no
337
+ artifact for the reporter to upload, which is consistent rather than missing.
338
+
339
+ ## Sharding
340
+
341
+ **Handled automatically - no extra setup needed.** Each shard is still a
342
+ separate process with its own reporter instance, but Squally now correlates
343
+ them itself: all shards of one CI run are grouped together and shown as one
344
+ run once every shard has finished, with the flip side of that guarantee being
345
+ that the run only completes once every shard *has* reported.
346
+
347
+ Correlation is based on an id read from your CI provider's own environment
348
+ (the "Shards grouped by" column above), so it costs you nothing on every
349
+ provider in that table except TeamCity. On TeamCity, on a CI system not in the
350
+ table, or when you shard locally across several separate `--shard`
351
+ invocations, set `SQUALLY_CI_RUN_ID` to the same value for every shard of one
352
+ run - anything unique per CI run and shared by its shards works, e.g. a build
353
+ number your CI system already exposes.
354
+
355
+ **Several jobs in one CI run need their own key (0.11.0).** The id is shared
356
+ by everything in one CI run, so two jobs that are *not* shards of each other -
357
+ a GitHub Actions matrix over browsers run without `--shard`, or an `api` and a
358
+ `ui` suite - would report into the same Squally run. Give each job its own
359
+ `SQUALLY_CI_JOB_KEY`, shared only by that job's own shards:
360
+
361
+ ```yaml
362
+ jobs:
363
+ test:
364
+ strategy:
365
+ matrix:
366
+ browser: [chromium, firefox]
367
+ env:
368
+ SQUALLY_CI_JOB_KEY: ${{ matrix.browser }}
369
+ steps:
370
+ - run: npx playwright test --project=${{ matrix.browser }}
371
+ ```
372
+
373
+ The reporter puts the key into the id - `<GITHUB_RUN_ID>-<key>-<GITHUB_RUN_ATTEMPT>`
374
+ on GitHub Actions, `<provider id>-<key>` elsewhere - so each job is its own run
375
+ in Squally, and a re-run of one job links to that job's own earlier attempt.
376
+ The key may hold letters, digits, `.`, `_` and `-`, at most 64 characters; an
377
+ invalid key is reported once and ignored, and your test run is unaffected.
378
+ Without a key the id is exactly what it was before 0.11.0. A suite that is
379
+ sharded *and* split into jobs uses the same key for all shards of one job:
380
+ `SQUALLY_CI_JOB_KEY: ${{ matrix.suite }}` beside `--shard=${{ matrix.shard }}/2`.
381
+
382
+ **Replaces the hand-built id.** Until 0.11.0 this section suggested
383
+ `SQUALLY_CI_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.browser }}`.
384
+ It still groups each job correctly, but Squally cannot tell its attempts apart,
385
+ so a re-run is never linked to the attempt before it. Switch to
386
+ `SQUALLY_CI_JOB_KEY`. An explicit `SQUALLY_CI_RUN_ID` still wins for the id;
387
+ the key is then only sent alongside it.
388
+
389
+ **When two jobs collide anyway, Squally refuses the second one at the start**
390
+ (squally-app with block 1b, reporter 0.11.0). The reporter prints one block,
391
+ sends nothing more for that job, and leaves your test run and its exit code
392
+ alone:
393
+
394
+ - `ci_run_collision` - another job already reports under this CI run id (and
395
+ shard). Give every job its own `SQUALLY_CI_JOB_KEY`.
396
+ - `ci_run_mixed` - the id would put a sharded job and an unsharded one, or
397
+ shards of different totals, into one run. The same fix.
398
+
399
+ Before 0.11.0 the second job was handed the first job's run instead, and its
400
+ results were refused once that run completed; a reporter older than 0.11.0
401
+ still behaves that way, and prints "Squally has already completed run …".
402
+
403
+ **Re-runs.** On GitHub Actions every re-run attempt is its own run in Squally,
404
+ linked to the attempt before it ("Re-run of #N"). "Re-run failed jobs" runs only
405
+ the shards that did not pass; that attempt waits only for those shards,
406
+ completes once they have reported, and passes or fails on the tests they ran -
407
+ the reused shards are neither waited for nor counted, and the run page shows
408
+ them as "passed in #N · not re-run". A re-run shard that never reports still
409
+ leaves the run incomplete after the run timeout.
410
+
411
+ Other CI systems re-run a job under the id of the run it belongs to - a GitLab
412
+ job retry, an Azure Pipelines re-run, a Bitbucket re-run of failed steps, a
413
+ Buildkite retry, a Travis CI restart. Since 0.11.0 Squally refuses such a
414
+ retry at the start with `ci_run_collision`: the slot is taken by the first
415
+ attempt. **`SQUALLY_CI_JOB_KEY` does not help here** - the retry is the same
416
+ job - so a retry on these systems is not reported; the first attempt stays the
417
+ run. A Drone restart is a new build with a new number, and so a new run in
418
+ Squally, not linked to the one it repeats.
419
+
420
+ **`playwright merge-reports` is not part of this and is not recommended** for
421
+ getting a single run in Squally - the automatic correlation above is the
422
+ supported path, and produces correct per-shard timing/branch/commit
423
+ information that a merge job cannot (see the historical note below). Nothing
424
+ stops you from using `merge-reports` for Playwright's own HTML report if you
425
+ want that separately; just don't point this reporter at the merged result
426
+ expecting Squally to treat it specially.
427
+
428
+ <details>
429
+ <summary>Why not just use `merge-reports`, historically</summary>
430
+
431
+ An earlier version of this reporter recommended `playwright merge-reports` as
432
+ the way to get one Squally run out of a sharded job. That path has a real
433
+ problem: branch, commit, commit message and author are all read on whichever
434
+ machine the reporter actually runs on - during a merge that is the merge job,
435
+ not the shards - and without a checkout there (and without the CI provider's
436
+ own commit variables) they all come out empty. A run with no branch is in
437
+ no branch's numbers and no branch filter finds it, so a "merged" run could
438
+ exist in the dashboard while missing from everything Squally measures per
439
+ branch: suite
440
+ stability, and each test's stability, flaky rate and failure rate on that
441
+ branch. The automatic correlation above reads that information from each
442
+ shard's own environment instead, so this problem does not arise.
443
+
444
+ </details>
445
+
446
+ ## Tested revision (optional)
447
+
448
+ The tested revision is the state of your **application** a run tested, as
449
+ opposed to the commit the run is reported under. Squally does not know it by
450
+ itself - it is **opt-in**, with one of these two:
451
+
452
+ ```bash
453
+ # The tests live in the same repo as the application: the commit this CI job
454
+ # checked out is the tested revision.
455
+ SAME_REPO_AS_APP=true
456
+
457
+ # The tests live in a separate repo (e.g. a dedicated E2E repo pointed at a
458
+ # deployed app): pass the tested revision itself - a commit SHA, tag, or
459
+ # branch name/ref.
460
+ TESTED_REVISION=v1.2.0
461
+ ```
462
+
463
+ - **`SAME_REPO_AS_APP=true`** sends the commit your job checked out: your CI
464
+ provider's own variable for it (`GITHUB_SHA`, `CI_COMMIT_SHA`,
465
+ `CIRCLE_SHA1`, `GIT_COMMIT`, `BUILD_SOURCEVERSION`, `BITBUCKET_COMMIT`,
466
+ `BUILDKITE_COMMIT`, `BUILD_VCS_NUMBER`, `CODEBUILD_RESOLVED_SOURCE_VERSION`,
467
+ `TRAVIS_COMMIT`, `DRONE_COMMIT_SHA`), or `git rev-parse HEAD` where there is
468
+ none - including local runs. Only a commit SHA is sent; if neither yields
469
+ one, the run has no tested revision.
470
+ - **On a pull request the two commits differ, on purpose.** The run is
471
+ reported under the pull request's head commit (see the provider table
472
+ above) - that is what Squally's check and PR comment need. The tested
473
+ revision is the merge commit CI checked out, because that is what the tests
474
+ ran against. On a Bitbucket pull request and a GitHub `pull_request_target`
475
+ run it comes from `git rev-parse HEAD`.
476
+ - **`TESTED_REVISION` wins** when both are set.
477
+ - Neither variable is required - without them, test results are still sent
478
+ and stored exactly as before.
479
+
480
+ **The value is normalised before it is stored**, so that the same revision
481
+ written two ways still compares equal: it is lowercased, and a leading `v` or a
482
+ `refs/heads/` prefix is removed. The example above is exactly such a case -
483
+ `v1.2.0` is stored as `1.2.0`. The reporter says so once per run when it
484
+ happens, so a value that looks different in the dashboard is not a mystery.
485
+
486
+ ## Troubleshooting
487
+
488
+ Set `SQUALLY_DEBUG=1` to print detail that is otherwise kept quiet - the
489
+ browser-console extraction (how many messages were found, or that an attempt's
490
+ trace held none) and, since 0.11.0, a CI run link that was not sent because it
491
+ was not an `https://` URL. Everything that is *not* ordinary (a trace that
492
+ cannot be read, an unsupported schema version, a rejected upload) is printed
493
+ without it.
494
+
495
+ ## Support
496
+
497
+ Found a bug or have a question? Please open an issue on
498
+ [GitHub Issues](https://github.com/squallydev-admin/squally-reporter/issues) -
499
+ that's the primary way to report problems or ask questions. For anything you
500
+ would rather not post publicly, write to
501
+ [support@squally.dev](mailto:support@squally.dev).
502
+
503
+ ## License
504
+
505
+ MIT - see [LICENSE](./LICENSE).