@e2edev/github 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TesterArmy, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,32 @@
1
+ # @e2edev/github
2
+
3
+ The GitHub reporter for [`@e2edev/e2e`](https://www.npmjs.com/package/@e2edev/e2e).
4
+ From GitHub Actions, every run becomes one pull request comment, edited in
5
+ place on reruns, and the same text lands in the job summary.
6
+
7
+ ```bash
8
+ npm install --save-dev @e2edev/github
9
+ ```
10
+
11
+ ```ts title="e2e.config.ts"
12
+ import type { E2EConfig } from '@e2edev/e2e';
13
+ import { playwright } from '@e2edev/playwright';
14
+ import { github } from '@e2edev/github';
15
+
16
+ export default {
17
+ targets: [{ engine: playwright({ url: 'http://localhost:3000' }) }],
18
+ reporters: ['list', github()],
19
+ } satisfies E2EConfig;
20
+ ```
21
+
22
+ The job needs `permissions: pull-requests: write` and the step that runs
23
+ `e2e` needs `GITHUB_TOKEN: ${{ github.token }}` in its `env`. Matrix replicas
24
+ of one job pass a `key` so their comments stay apart. When the reporter cannot
25
+ post (another CI, a push, a fork's read-only token) it says why in one summary
26
+ row and never changes the run's exit code.
27
+
28
+ Full documentation lives at [e2e-docs.vercel.app/github](https://e2e-docs.vercel.app/github).
29
+
30
+ ## License
31
+
32
+ MIT
@@ -0,0 +1,47 @@
1
+ /**
2
+ * What GitHub Actions tells a job about itself, read from the environment
3
+ * the runner sets: the repository, the pull request the job runs for, the
4
+ * commit to link sources at, the token, and where the job summary is written.
5
+ *
6
+ * The pull request number comes from the event payload first, since
7
+ * `pull_request`, `pull_request_target`, and a PR's `issue_comment` all carry
8
+ * it there, and from `GITHUB_REF` (`refs/pull/N/merge`) when the payload has
9
+ * none. Nothing here is read until the run has finished.
10
+ */
11
+ export interface ActionsContext {
12
+ /** `owner/repo`. */
13
+ readonly repository: string;
14
+ /** `https://github.com`, or the GitHub Enterprise Server host. */
15
+ readonly serverUrl: string;
16
+ /** `https://api.github.com`, or the Enterprise Server API root. */
17
+ readonly apiUrl: string;
18
+ /** The workflow run page, where uploaded artifacts live. */
19
+ readonly runUrl: string;
20
+ readonly eventName: string;
21
+ readonly workflow: string;
22
+ readonly job: string;
23
+ /**
24
+ * The commit sources are linked at: the pull request's head when the event
25
+ * payload names it, the checked-out SHA when the event is not about a pull
26
+ * request, and nothing on an `issue_comment`, whose SHA is the default
27
+ * branch and would link every source line to the wrong revision.
28
+ */
29
+ readonly sha: string | undefined;
30
+ readonly pullRequest: number | undefined;
31
+ /** `GITHUB_TOKEN`, or `GH_TOKEN` the way the `gh` CLI reads it. */
32
+ readonly token: string | undefined;
33
+ /** The file `GITHUB_STEP_SUMMARY` names, when the runner provides one. */
34
+ readonly stepSummaryPath: string | undefined;
35
+ }
36
+ export interface ActionsDeps {
37
+ readonly env: NodeJS.ProcessEnv;
38
+ readonly readFile: (file: string) => Promise<string>;
39
+ }
40
+ /**
41
+ * The Actions context of the current job, or undefined outside GitHub
42
+ * Actions. On Actions the runner always sets the repository, run id, event
43
+ * name, workflow, and job; a job missing one is broken, not "not Actions",
44
+ * and says so.
45
+ */
46
+ export declare function detectActions(deps: ActionsDeps): Promise<ActionsContext | undefined>;
47
+ //# sourceMappingURL=actions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,WAAW,cAAc;IAC7B,oBAAoB;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,mEAAmE;IACnE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,0EAA0E;IAC1E,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9C;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CACtD;AAsDD;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CA0B1F"}
@@ -0,0 +1,90 @@
1
+ /**
2
+ * What GitHub Actions tells a job about itself, read from the environment
3
+ * the runner sets: the repository, the pull request the job runs for, the
4
+ * commit to link sources at, the token, and where the job summary is written.
5
+ *
6
+ * The pull request number comes from the event payload first, since
7
+ * `pull_request`, `pull_request_target`, and a PR's `issue_comment` all carry
8
+ * it there, and from `GITHUB_REF` (`refs/pull/N/merge`) when the payload has
9
+ * none. Nothing here is read until the run has finished.
10
+ */
11
+ import { isRecord } from './json.js';
12
+ /** A variable's trimmed value; unset and blank both read as undefined. */
13
+ function envValue(env, name) {
14
+ const raw = env[name];
15
+ return raw === undefined || raw.trim() === '' ? undefined : raw.trim();
16
+ }
17
+ function pullRequestNumber(value) {
18
+ return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined;
19
+ }
20
+ /**
21
+ * Reads the pull request out of the webhook payload, or nothing when the
22
+ * payload is missing, unreadable, or not about a pull request.
23
+ */
24
+ async function readEvent(path, deps) {
25
+ if (path === undefined)
26
+ return undefined;
27
+ let payload;
28
+ try {
29
+ payload = JSON.parse(await deps.readFile(path));
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ if (!isRecord(payload))
35
+ return undefined;
36
+ const pull = payload['pull_request'];
37
+ if (isRecord(pull)) {
38
+ const head = pull['head'];
39
+ return {
40
+ number: pullRequestNumber(pull['number']),
41
+ headSha: isRecord(head) && typeof head['sha'] === 'string' ? head['sha'] : undefined,
42
+ };
43
+ }
44
+ // An `issue_comment` on a pull request: the issue carries a `pull_request`
45
+ // link but not the head commit, and GITHUB_SHA is the default branch here.
46
+ const issue = payload['issue'];
47
+ if (isRecord(issue) && isRecord(issue['pull_request'])) {
48
+ return { number: pullRequestNumber(issue['number']), headSha: undefined };
49
+ }
50
+ return undefined;
51
+ }
52
+ /** The number in `refs/pull/N/merge` or `refs/pull/N/head`. */
53
+ function pullFromRef(ref) {
54
+ const match = ref === undefined ? null : /^refs\/pull\/(\d+)\//.exec(ref);
55
+ return match === null ? undefined : pullRequestNumber(Number(match[1]));
56
+ }
57
+ /**
58
+ * The Actions context of the current job, or undefined outside GitHub
59
+ * Actions. On Actions the runner always sets the repository, run id, event
60
+ * name, workflow, and job; a job missing one is broken, not "not Actions",
61
+ * and says so.
62
+ */
63
+ export async function detectActions(deps) {
64
+ const value = (name) => envValue(deps.env, name);
65
+ if (value('GITHUB_ACTIONS') === undefined)
66
+ return undefined;
67
+ const required = (name) => {
68
+ const found = value(name);
69
+ if (found === undefined)
70
+ throw new Error(`GITHUB_ACTIONS is set but ${name} is not`);
71
+ return found;
72
+ };
73
+ const repository = required('GITHUB_REPOSITORY');
74
+ const serverUrl = (value('GITHUB_SERVER_URL') ?? 'https://github.com').replace(/\/+$/, '');
75
+ const event = await readEvent(value('GITHUB_EVENT_PATH'), deps);
76
+ return {
77
+ repository,
78
+ serverUrl,
79
+ apiUrl: (value('GITHUB_API_URL') ?? 'https://api.github.com').replace(/\/+$/, ''),
80
+ runUrl: `${serverUrl}/${repository}/actions/runs/${required('GITHUB_RUN_ID')}`,
81
+ eventName: required('GITHUB_EVENT_NAME'),
82
+ workflow: required('GITHUB_WORKFLOW'),
83
+ job: required('GITHUB_JOB'),
84
+ sha: event === undefined ? value('GITHUB_SHA') : event.headSha,
85
+ pullRequest: event?.number ?? pullFromRef(value('GITHUB_REF')),
86
+ token: value('GITHUB_TOKEN') ?? value('GH_TOKEN'),
87
+ stepSummaryPath: value('GITHUB_STEP_SUMMARY'),
88
+ };
89
+ }
90
+ //# sourceMappingURL=actions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The report-1 document as one pull request comment: a headline with the
3
+ * counts, the run-level errors, a table of every test that did not simply
4
+ * pass, the passed tests folded away, and where the evidence is.
5
+ *
6
+ * Everything a test wrote (titles, error messages) is untrusted text on its
7
+ * way into markdown GitHub renders, so cells are escaped and clipped. The
8
+ * builder is exported so a hosted run page can render the same comment.
9
+ */
10
+ import type { Report } from '@e2edev/e2e';
11
+ export interface CommentOptions {
12
+ /** A hidden HTML comment the reporter finds its own comment by; left out of a job summary. */
13
+ readonly marker?: string | undefined;
14
+ /** Where the run's artifacts can be fetched: the workflow run page, or a hosted run page. */
15
+ readonly artifactsUrl?: string | undefined;
16
+ /** A link to a source line, when the commit is known. */
17
+ readonly sourceUrl?: ((file: string, line: number) => string) | undefined;
18
+ }
19
+ /**
20
+ * A marker is what a rerun finds the comment by, so it can never be cut. With
21
+ * it bounded, the head and footer together stay far under the budget and the
22
+ * body is the only part that ever gives way.
23
+ */
24
+ export declare const MAX_MARKER_CHARS = 1024;
25
+ /** The artifacts link sits in the footer beside the marker, so it is bounded the same way. */
26
+ export declare const MAX_URL_CHARS = 2048;
27
+ /** Renders the run as a pull request comment body. */
28
+ export declare function renderComment(report: Report, options?: CommentOptions): string;
29
+ //# sourceMappingURL=comment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment.d.ts","sourceRoot":"","sources":["../src/comment.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAQ1C,MAAM,WAAW,cAAc;IAC7B,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,6FAA6F;IAC7F,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3C,yDAAyD;IACzD,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,GAAG,SAAS,CAAC;CAC3E;AAWD;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AACtC,8FAA8F;AAC9F,eAAO,MAAM,aAAa,OAAQ,CAAC;AA2InC,sDAAsD;AACtD,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,CA2ElF"}
@@ -0,0 +1,209 @@
1
+ /**
2
+ * The report-1 document as one pull request comment: a headline with the
3
+ * counts, the run-level errors, a table of every test that did not simply
4
+ * pass, the passed tests folded away, and where the evidence is.
5
+ *
6
+ * Everything a test wrote (titles, error messages) is untrusted text on its
7
+ * way into markdown GitHub renders, so cells are escaped and clipped. The
8
+ * builder is exported so a hosted run page can render the same comment.
9
+ */
10
+ import { stripVTControlCharacters } from 'node:util';
11
+ /** GitHub rejects a comment body past 65536 characters; stay clear of it. */
12
+ const MAX_BODY_CHARS = 60_000;
13
+ /** Reader caps, not size guards: past these the comment says how many more there are. */
14
+ const MAX_TABLE_ROWS = 50;
15
+ const MAX_RUN_ERRORS = 20;
16
+ const MAX_PASSED_LINES = 200;
17
+ const MAX_FOOTER_TARGETS = 8;
18
+ const MAX_CELL_CHARS = 240;
19
+ const TRUNCATED_NOTE = "_Comment truncated to fit GitHub's size limit; the full report is in the run artifacts._";
20
+ /**
21
+ * A marker is what a rerun finds the comment by, so it can never be cut. With
22
+ * it bounded, the head and footer together stay far under the budget and the
23
+ * body is the only part that ever gives way.
24
+ */
25
+ export const MAX_MARKER_CHARS = 1_024;
26
+ /** The artifacts link sits in the footer beside the marker, so it is bounded the same way. */
27
+ export const MAX_URL_CHARS = 2_048;
28
+ /** C0/C1 control characters except tab and newline; ANSI sequences are stripped first. */
29
+ // oxlint-disable-next-line no-control-regex -- the control range is the point
30
+ const CONTROL_PATTERN = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
31
+ /** One line of plain text: no color codes, no control characters, no line breaks. */
32
+ function plain(text) {
33
+ return stripVTControlCharacters(text).replace(CONTROL_PATTERN, '').replace(/\s+/g, ' ').trim();
34
+ }
35
+ /** Clips by code point, so an emoji at the cut never becomes a lone surrogate. */
36
+ function clip(text, max) {
37
+ const points = [...text];
38
+ return points.length <= max ? text : `${points.slice(0, max - 1).join('')}…`;
39
+ }
40
+ /**
41
+ * Text safe inside a table cell: markdown that could open a construct is
42
+ * escaped, angle brackets become entities so no HTML gets through, and the
43
+ * cell separator is escaped. An underscore stays: inside a word GitHub
44
+ * never reads it as emphasis, and error codes are full of them.
45
+ */
46
+ function cell(text, max = MAX_CELL_CHARS) {
47
+ return clip(plain(text), max)
48
+ .replace(/[\\`*[\]~|]/g, (char) => `\\${char}`)
49
+ .replaceAll('<', '&lt;')
50
+ .replaceAll('>', '&gt;');
51
+ }
52
+ function formatDuration(ms) {
53
+ if (ms < 1_000)
54
+ return `${Math.round(ms)}ms`;
55
+ if (ms < 60_000)
56
+ return `${(ms / 1_000).toFixed(1)}s`;
57
+ // Round to whole seconds first, so 119.5 s is 2m 0s and never 1m 60s.
58
+ const total = Math.round(ms / 1_000);
59
+ return `${Math.floor(total / 60)}m ${total % 60}s`;
60
+ }
61
+ function plural(count, noun) {
62
+ return `${count} ${noun}${count === 1 ? '' : 's'}`;
63
+ }
64
+ function outcome(result, groups) {
65
+ const attempts = result.serialGroupId === undefined ? result.attempts : (groups.get(result.serialGroupId)?.attempts ?? []);
66
+ const last = attempts.at(-1);
67
+ const member = result.serialGroupId === undefined || last === undefined || !('members' in last)
68
+ ? undefined
69
+ : last.members.find((candidate) => candidate.testId === result.testId);
70
+ return {
71
+ durationMs: member?.durationMs ?? last?.durationMs ?? 0,
72
+ error: member?.error ?? last?.error,
73
+ failedAttempts: attempts.slice(0, -1).filter((attempt) => attempt.status !== 'passed').length,
74
+ artifactKinds: kinds(attempts.flatMap((attempt) => attempt.artifacts)),
75
+ };
76
+ }
77
+ /** Screenshots first, then the recording, then the trace; the compiler fails when a kind is missing here. */
78
+ const KIND_RANK = { screenshot: 0, video: 1, trace: 2, download: 3, log: 4 };
79
+ function kinds(artifacts) {
80
+ return [...new Set(artifacts.map((artifact) => artifact.kind))].toSorted((a, b) => KIND_RANK[a] - KIND_RANK[b]);
81
+ }
82
+ function bucket(status) {
83
+ return status === 'passed' || status === 'flaky' || status === 'skipped' ? status : 'failed';
84
+ }
85
+ const ICON = { failed: '🔴', flaky: '⚠️', skipped: '⏭️', passed: '🟢' };
86
+ /** The order the table lists what did not pass; passed tests fold away below it. */
87
+ const LISTED = ['failed', 'flaky', 'skipped'];
88
+ function groupByBucket(results) {
89
+ const groups = new Map();
90
+ for (const result of results) {
91
+ const key = bucket(result.status);
92
+ groups.set(key, [...(groups.get(key) ?? []), result]);
93
+ }
94
+ return groups;
95
+ }
96
+ function headline(run, groups) {
97
+ const parts = ['failed', 'flaky', 'passed', 'skipped']
98
+ .filter((key) => (groups.get(key)?.length ?? 0) > 0)
99
+ .map((key) => `${groups.get(key)?.length} ${key}`);
100
+ const summary = parts.length > 0 ? parts.join(', ') : run.errors.length > 0 ? 'no tests ran' : 'no tests selected';
101
+ return `### ${run.status === 'passed' ? ICON.passed : ICON.failed} e2e: ${summary}`;
102
+ }
103
+ function errorText(error) {
104
+ const phase = error.phase === undefined ? '' : ` (${plain(error.phase)})`;
105
+ return `**${cell(error.code, 128)}**${phase} ${cell(error.message)}`.trim();
106
+ }
107
+ function outcomeCell(result, final) {
108
+ if (result.status === 'flaky' && final.failedAttempts > 0) {
109
+ return `flaky: passed after ${plural(final.failedAttempts, 'failed attempt')}`;
110
+ }
111
+ if (result.status === 'skipped')
112
+ return `skipped: ${cell(result.skip?.reason ?? 'skipped')}`;
113
+ return final.error === undefined ? cell(result.status) : errorText(final.error);
114
+ }
115
+ function evidenceCell(final, options) {
116
+ if (final.artifactKinds.length === 0)
117
+ return '';
118
+ const text = final.artifactKinds.join(', ');
119
+ return options.artifactsUrl === undefined ? text : `[${text}](${options.artifactsUrl})`;
120
+ }
121
+ function footer(run, options) {
122
+ const duration = formatDuration(Date.parse(run.finishedAt) - Date.parse(run.startedAt));
123
+ const shown = run.targets.slice(0, MAX_FOOTER_TARGETS).map((target) => cell(target.id, 64));
124
+ if (run.targets.length > shown.length)
125
+ shown.push(`and ${run.targets.length - shown.length} more`);
126
+ const targets = shown.length === 0 ? '' : ` (${shown.join(', ')})`;
127
+ return [
128
+ ...(options.artifactsUrl === undefined
129
+ ? []
130
+ : [`Screenshots, traces, and recordings: [run artifacts](${options.artifactsUrl}).`]),
131
+ `<sub>e2e ${cell(run.runner.version, 64)} · ${duration} · ${plural(run.targets.length, 'target')}${targets}</sub>`,
132
+ ];
133
+ }
134
+ /** Renders the run as a pull request comment body. */
135
+ export function renderComment(report, options = {}) {
136
+ if (options.marker !== undefined && options.marker.length > MAX_MARKER_CHARS) {
137
+ throw new Error(`renderComment: marker must be at most ${MAX_MARKER_CHARS} characters, got ${options.marker.length}`);
138
+ }
139
+ if (options.artifactsUrl !== undefined && options.artifactsUrl.length > MAX_URL_CHARS) {
140
+ throw new Error(`renderComment: artifactsUrl must be at most ${MAX_URL_CHARS} characters, got ${options.artifactsUrl.length}`);
141
+ }
142
+ const run = report.run;
143
+ const serialGroups = new Map(run.serialGroups.map((group) => [group.id, group]));
144
+ const groups = groupByBucket(run.results);
145
+ const manyTargets = run.targets.length > 1;
146
+ /** `file:line › suite › title (target)`, the location linked when the commit is known. */
147
+ const name = (result, location) => {
148
+ const title = result.titlePath.map((part) => cell(part, 120)).join(' › ');
149
+ return `${location} › ${title}${manyTargets ? ` (${cell(result.targetId, 64)})` : ''}`;
150
+ };
151
+ const linked = (result) => {
152
+ const location = `${result.source.file}:${result.source.line}`;
153
+ return options.sourceUrl === undefined
154
+ ? `\`${plain(location).replaceAll('`', '')}\``
155
+ : `[${cell(location)}](${options.sourceUrl(result.source.file, result.source.line)})`;
156
+ };
157
+ // Body parts in priority order; each is one line, except the passed list,
158
+ // which is one block since a `<details>` cannot be cut halfway.
159
+ const errors = run.errors.slice(0, MAX_RUN_ERRORS).map((error) => `> ${errorText(error)}`);
160
+ if (run.errors.length > errors.length)
161
+ errors.push(`> and ${run.errors.length - errors.length} more`);
162
+ const listed = LISTED.flatMap((key) => groups.get(key) ?? []);
163
+ const rows = listed.slice(0, MAX_TABLE_ROWS).map((result) => {
164
+ const final = outcome(result, serialGroups);
165
+ return `| ${ICON[bucket(result.status)]} | ${name(result, linked(result))} | ${outcomeCell(result, final)} | ${evidenceCell(final, options)} |`;
166
+ });
167
+ const table = rows.length === 0 ? [] : ['| | Test | Outcome | Evidence |', '| --- | --- | --- | --- |', ...rows];
168
+ if (listed.length > rows.length)
169
+ table.push(`| | and ${listed.length - rows.length} more | | |`);
170
+ const passed = groups.get('passed') ?? [];
171
+ const passedBlock = passed.length === 0
172
+ ? []
173
+ : [
174
+ [
175
+ '<details>',
176
+ `<summary>${plural(passed.length, 'passed test')}</summary>`,
177
+ '',
178
+ ...passed
179
+ .slice(0, MAX_PASSED_LINES)
180
+ .map((result) => `- ${name(result, cell(result.file, 200))} (${formatDuration(outcome(result, serialGroups).durationMs)})`),
181
+ ...(passed.length > MAX_PASSED_LINES ? [`- and ${passed.length - MAX_PASSED_LINES} more`] : []),
182
+ '</details>',
183
+ ].join('\n'),
184
+ ];
185
+ const sections = [errors, table, passedBlock].filter((section) => section.length > 0);
186
+ const head = [...(options.marker === undefined ? [] : [options.marker]), headline(run, groups), ''];
187
+ const tail = footer(run, options);
188
+ const render = (body) => `${[...head, ...body, ...tail].join('\n')}\n`;
189
+ // Greedy fit: keep whole parts in order while they fit, then say what was cut.
190
+ let size = render([TRUNCATED_NOTE]).length;
191
+ const body = [];
192
+ let cut = false;
193
+ for (const section of sections) {
194
+ for (const part of section) {
195
+ if (size + part.length + 1 > MAX_BODY_CHARS) {
196
+ cut = true;
197
+ break;
198
+ }
199
+ body.push(part);
200
+ size += part.length + 1;
201
+ }
202
+ if (cut)
203
+ break;
204
+ body.push('');
205
+ size += 1;
206
+ }
207
+ return render(cut ? [...body, TRUNCATED_NOTE] : body);
208
+ }
209
+ //# sourceMappingURL=comment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment.js","sourceRoot":"","sources":["../src/comment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * `@e2edev/github` public surface: the `github()` reporter factory and the
3
+ * comment builder it uses. The reporter posts one pull request comment per
4
+ * run from GitHub Actions, through the `reporters` seam of `@e2edev/e2e`,
5
+ * and writes the same text to the job summary.
6
+ */
7
+ export { github } from './reporter.ts';
8
+ export type { GitHubOptions } from './reporter.ts';
9
+ export { renderComment, MAX_MARKER_CHARS, MAX_URL_CHARS } from './comment.ts';
10
+ export type { CommentOptions } from './comment.ts';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AACvC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC9E,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `@e2edev/github` public surface: the `github()` reporter factory and the
3
+ * comment builder it uses. The reporter posts one pull request comment per
4
+ * run from GitHub Actions, through the `reporters` seam of `@e2edev/e2e`,
5
+ * and writes the same text to the job summary.
6
+ */
7
+ export { github } from './reporter.js';
8
+ export { renderComment, MAX_MARKER_CHARS, MAX_URL_CHARS } from './comment.js';
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
package/dist/json.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** A parsed JSON object, as opposed to an array, a primitive, or null. */
2
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
3
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAEzE"}
package/dist/json.js ADDED
@@ -0,0 +1,5 @@
1
+ /** A parsed JSON object, as opposed to an array, a primitive, or null. */
2
+ export function isRecord(value) {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
4
+ }
5
+ //# sourceMappingURL=json.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.js","sourceRoot":"","sources":["../src/json.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC"}
package/dist/post.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * One comment per pull request, kept current: the reporter looks for the
3
+ * comment carrying its marker and edits it, and creates one only when none
4
+ * exists. The REST calls are the three GitHub documents them as: list the
5
+ * issue's comments, update a comment, create a comment.
6
+ *
7
+ * Every request carries the reporter's abort signal. Anything GitHub answers
8
+ * outside the protocol is an error naming itself; the runner prints one line.
9
+ */
10
+ export interface PostParams {
11
+ readonly fetch: typeof fetch;
12
+ readonly signal: AbortSignal;
13
+ /** `https://api.github.com`, or an Enterprise Server API root. */
14
+ readonly apiUrl: string;
15
+ readonly token: string;
16
+ /** `owner/repo`. */
17
+ readonly repository: string;
18
+ readonly pullRequest: number;
19
+ /** The hidden HTML comment that identifies the reporter's own comment. */
20
+ readonly marker: string;
21
+ readonly body: string;
22
+ }
23
+ /** Creates or updates the reporter's comment and resolves with its URL. */
24
+ export declare function upsertComment(params: PostParams): Promise<string>;
25
+ //# sourceMappingURL=post.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post.d.ts","sourceRoot":"","sources":["../src/post.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;IAC7B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,oBAAoB;IACpB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAqDD,2EAA2E;AAC3E,wBAAsB,aAAa,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAWvE"}
package/dist/post.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * One comment per pull request, kept current: the reporter looks for the
3
+ * comment carrying its marker and edits it, and creates one only when none
4
+ * exists. The REST calls are the three GitHub documents them as: list the
5
+ * issue's comments, update a comment, create a comment.
6
+ *
7
+ * Every request carries the reporter's abort signal. Anything GitHub answers
8
+ * outside the protocol is an error naming itself; the runner prints one line.
9
+ */
10
+ import { isRecord } from './json.js';
11
+ const PER_PAGE = 100;
12
+ /**
13
+ * The list is followed until a short page says it is exhausted. The cap is a
14
+ * guard against a server that never sends one, not a limit any pull request
15
+ * reaches: ten thousand comments.
16
+ */
17
+ const MAX_PAGES = 100;
18
+ const USER_AGENT = '@e2edev/github';
19
+ /** One GitHub REST call; anything but success is an error the runner can print. */
20
+ async function request(params, method, route, body) {
21
+ const response = await params.fetch(`${params.apiUrl}${route}`, {
22
+ method,
23
+ headers: {
24
+ authorization: `Bearer ${params.token}`,
25
+ accept: 'application/vnd.github+json',
26
+ 'x-github-api-version': '2022-11-28',
27
+ 'user-agent': USER_AGENT,
28
+ ...(body === undefined ? {} : { 'content-type': 'application/json' }),
29
+ },
30
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
31
+ signal: params.signal,
32
+ });
33
+ if (response.ok)
34
+ return response.json().catch(() => undefined);
35
+ const detail = (await response.text().catch(() => '')).slice(0, 200);
36
+ if (response.status === 401)
37
+ throw new Error('GitHub rejected the token (GITHUB_TOKEN or GH_TOKEN)');
38
+ if (response.status === 403 || response.status === 404) {
39
+ throw new Error(`the token cannot comment on ${params.repository}#${params.pullRequest}: a pull request from a fork runs with a read-only token, and the job needs \`permissions: pull-requests: write\``);
40
+ }
41
+ throw new Error(`GitHub responded ${response.status} to ${method} ${route}${detail === '' ? '' : `: ${detail}`}`);
42
+ }
43
+ /** The id of the comment carrying the marker, or undefined when no page has one. */
44
+ async function findComment(params, issue) {
45
+ for (let page = 1; page <= MAX_PAGES; page += 1) {
46
+ const comments = await request(params, 'GET', `${issue}/comments?per_page=${PER_PAGE}&page=${page}`);
47
+ if (!Array.isArray(comments))
48
+ throw new Error('GitHub answered the comment list with a body that is not a list');
49
+ const found = comments.find((comment) => isRecord(comment) && typeof comment['body'] === 'string' && comment['body'].includes(params.marker));
50
+ if (isRecord(found) && typeof found['id'] === 'number')
51
+ return found['id'];
52
+ if (comments.length < PER_PAGE)
53
+ return undefined;
54
+ }
55
+ return undefined;
56
+ }
57
+ /** Creates or updates the reporter's comment and resolves with its URL. */
58
+ export async function upsertComment(params) {
59
+ const issue = `/repos/${params.repository}/issues/${params.pullRequest}`;
60
+ const existing = await findComment(params, issue);
61
+ const created = existing === undefined
62
+ ? await request(params, 'POST', `${issue}/comments`, { body: params.body })
63
+ : await request(params, 'PATCH', `/repos/${params.repository}/issues/comments/${existing}`, { body: params.body });
64
+ if (!isRecord(created) || typeof created['html_url'] !== 'string') {
65
+ throw new Error('GitHub answered the comment without its html_url');
66
+ }
67
+ return created['html_url'];
68
+ }
69
+ //# sourceMappingURL=post.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"post.js","sourceRoot":"","sources":["../src/post.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { FinishedRun, Reporter, ReporterSummary } from '@e2edev/e2e';
2
+ import { type ActionsDeps } from './actions.ts';
3
+ export interface GitHubOptions {
4
+ /**
5
+ * Tells one job's comment apart from another's when the workflow, job, and
6
+ * project are the same, as in a matrix: `key: process.env.MATRIX_BROWSER`.
7
+ * Without it, matrix replicas of one job share a comment and the last one
8
+ * to finish wins. Never the run id: a rerun must find the old comment.
9
+ */
10
+ key?: string;
11
+ }
12
+ /** What the reporter touches outside itself, so tests run it against fakes. */
13
+ export interface ReportDeps extends ActionsDeps {
14
+ readonly fetch: typeof fetch;
15
+ readonly appendFile: (file: string, text: string) => Promise<void>;
16
+ }
17
+ /**
18
+ * The GitHub reporter. Add it to `reporters` beside the built-in ids:
19
+ *
20
+ * ```ts
21
+ * import { github } from '@e2edev/github';
22
+ * export default { targets, reporters: ['list', github()] } satisfies E2EConfig;
23
+ * ```
24
+ *
25
+ * On GitHub Actions it writes the run to the job summary and, for a pull
26
+ * request, posts or updates one comment. Everything it reads (the token, the
27
+ * event, the repository) is read when the run finishes, never when the config
28
+ * loads, so constructing it has no side effects.
29
+ */
30
+ export declare function github(options?: GitHubOptions): Reporter;
31
+ /** Reports one finished run and resolves with the summary rows describing what happened. */
32
+ export declare function reportRun(run: FinishedRun, signal: AbortSignal, options: GitHubOptions, deps: ReportDeps): Promise<ReporterSummary>;
33
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAsC,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAMpF,MAAM,WAAW,aAAa;IAC5B;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,+EAA+E;AAC/E,MAAM,WAAW,UAAW,SAAQ,WAAW;IAC7C,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACpE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,QAAQ,CAW5D;AA+DD,4FAA4F;AAC5F,wBAAsB,SAAS,CAC7B,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,WAAW,EACnB,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,eAAe,CAAC,CAyB1B"}
@@ -0,0 +1,107 @@
1
+ import { appendFile, readFile } from 'node:fs/promises';
2
+ import { detectActions } from './actions.js';
3
+ import { renderComment } from './comment.js';
4
+ import { upsertComment } from './post.js';
5
+ const SUMMARY_LABEL = 'GitHub';
6
+ /**
7
+ * The GitHub reporter. Add it to `reporters` beside the built-in ids:
8
+ *
9
+ * ```ts
10
+ * import { github } from '@e2edev/github';
11
+ * export default { targets, reporters: ['list', github()] } satisfies E2EConfig;
12
+ * ```
13
+ *
14
+ * On GitHub Actions it writes the run to the job summary and, for a pull
15
+ * request, posts or updates one comment. Everything it reads (the token, the
16
+ * event, the repository) is read when the run finishes, never when the config
17
+ * loads, so constructing it has no side effects.
18
+ */
19
+ export function github(options = {}) {
20
+ return {
21
+ name: 'github',
22
+ onRunFinished: (run, signal) => reportRun(run, signal, options, {
23
+ fetch: globalThis.fetch,
24
+ env: process.env,
25
+ readFile: (file) => readFile(file, 'utf8'),
26
+ appendFile: (file, text) => appendFile(file, text, 'utf8'),
27
+ }),
28
+ };
29
+ }
30
+ /**
31
+ * Each encoded marker field is cut to this many characters, so four fields
32
+ * and their names stay under `MAX_MARKER_CHARS` whatever the input. The cut
33
+ * never lands inside a percent escape, and it is the same on every run.
34
+ */
35
+ const MAX_MARKER_FIELD_CHARS = 200;
36
+ function markerField(value) {
37
+ const encoded = encodeURIComponent(value);
38
+ if (encoded.length <= MAX_MARKER_FIELD_CHARS)
39
+ return encoded;
40
+ return encoded.slice(0, MAX_MARKER_FIELD_CHARS).replace(/%[0-9A-F]?$/, '');
41
+ }
42
+ /**
43
+ * The marker one job's comment carries, so two workflows or jobs each keep
44
+ * their own. Every field is cut the same way on every run, so a rerun
45
+ * derives the same marker and finds its comment.
46
+ */
47
+ function commentMarker(run, context, key) {
48
+ const fields = {
49
+ project: run.report.run.project.id,
50
+ workflow: context.workflow,
51
+ job: context.job,
52
+ key,
53
+ };
54
+ const parts = Object.entries(fields).flatMap(([name, value]) => value === undefined || value === '' ? [] : [`${name}=${markerField(value)}`]);
55
+ return `<!-- e2e-github ${parts.join(' ')} -->`;
56
+ }
57
+ function sourceUrl(context) {
58
+ const sha = context.sha;
59
+ if (sha === undefined)
60
+ return undefined;
61
+ return (file, line) => `${context.serverUrl}/${context.repository}/blob/${sha}/${file.split('/').map(encodeURIComponent).join('/')}#L${line}`;
62
+ }
63
+ function posting(context) {
64
+ if (context.pullRequest === undefined)
65
+ return { skipped: `${context.eventName} is not a pull request` };
66
+ if (context.token === undefined) {
67
+ return { skipped: "set GITHUB_TOKEN in the step's env (GITHUB_TOKEN: ${{ github.token }})" };
68
+ }
69
+ return { token: context.token, pullRequest: context.pullRequest };
70
+ }
71
+ async function writeSummary(context, text, deps) {
72
+ if (context.stepSummaryPath === undefined)
73
+ return 'none';
74
+ try {
75
+ await deps.appendFile(context.stepSummaryPath, `${text}\n`);
76
+ return 'written';
77
+ }
78
+ catch (error) {
79
+ return { failed: error instanceof Error ? error.message : String(error) };
80
+ }
81
+ }
82
+ /** Reports one finished run and resolves with the summary rows describing what happened. */
83
+ export async function reportRun(run, signal, options, deps) {
84
+ const context = await detectActions(deps);
85
+ if (context === undefined)
86
+ return [{ label: SUMMARY_LABEL, text: 'not posted: not running on GitHub Actions' }];
87
+ const links = { artifactsUrl: context.runUrl, sourceUrl: sourceUrl(context) };
88
+ const summary = await writeSummary(context, renderComment(run.report, links), deps);
89
+ const post = posting(context);
90
+ const marker = commentMarker(run, context, options.key);
91
+ const lead = 'skipped' in post
92
+ ? `not posted: ${post.skipped}${summary === 'written' ? '; written to the job summary' : ''}`
93
+ : await upsertComment({
94
+ fetch: deps.fetch,
95
+ signal,
96
+ apiUrl: context.apiUrl,
97
+ repository: context.repository,
98
+ ...post,
99
+ marker,
100
+ body: renderComment(run.report, { ...links, marker }),
101
+ });
102
+ return [
103
+ { label: SUMMARY_LABEL, text: lead },
104
+ ...(typeof summary === 'object' ? [{ label: SUMMARY_LABEL, text: `job summary not written: ${summary.failed}` }] : []),
105
+ ];
106
+ }
107
+ //# sourceMappingURL=reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.js","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@e2edev/github",
3
+ "version": "0.1.0",
4
+ "description": "GitHub reporter for e2e: one pull request comment per run, from GitHub Actions",
5
+ "keywords": [
6
+ "e2e",
7
+ "testing",
8
+ "agentic",
9
+ "end-to-end",
10
+ "reporter",
11
+ "github",
12
+ "github-actions",
13
+ "pull-request"
14
+ ],
15
+ "license": "MIT",
16
+ "contributors": [
17
+ "Oskar Kwaśniewski <oskar@tester.army>",
18
+ "Szymon Rybczak <szymon@tester.army>"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/tester-army/e2e.git",
23
+ "directory": "packages/github"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/tester-army/e2e/issues"
27
+ },
28
+ "homepage": "https://github.com/tester-army/e2e/tree/main/packages/github#readme",
29
+ "publishConfig": {
30
+ "registry": "https://registry.npmjs.org/",
31
+ "access": "restricted",
32
+ "provenance": false,
33
+ "tag": "beta"
34
+ },
35
+ "type": "module",
36
+ "exports": {
37
+ ".": {
38
+ "types": "./dist/index.d.ts",
39
+ "default": "./dist/index.js"
40
+ }
41
+ },
42
+ "files": [
43
+ "dist"
44
+ ],
45
+ "peerDependencies": {
46
+ "@e2edev/e2e": ">=0.9.0 <1"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "26.4.1",
50
+ "typescript": "7.0.2",
51
+ "vitest": "5.0.0",
52
+ "@e2edev/e2e": "0.10.0"
53
+ },
54
+ "engines": {
55
+ "node": ">=22.12.0"
56
+ },
57
+ "scripts": {
58
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc --project tsconfig.build.json",
59
+ "typecheck": "tsc --noEmit",
60
+ "test": "vitest run",
61
+ "test:watch": "vitest",
62
+ "test:unit": "vitest run tests/unit"
63
+ }
64
+ }