Repository: trezor/trezor-suite
Pull request #18505: New GitHub reporter for both manual and automated test runs
Author: login: Vere-Grey
Created: 2025-04-23T08:26:49Z | Merged: 2025-04-26T06:43:45Z

The report at this stage:
- is run only locally by cmnd `yarn test:e2e:web:report` or yarn `test:e2e:desktop:report`
- requires github token in .env
- Creates QA team project based on now hardcoded name or finds existsing one
- The project has fields based on annotations definitions in our repo
- reports both manual and automated runs, draft issue is created in the project for every test
- retries failed request but there is not logic for checking if the test already exists

Next steps:
validation with Ondra
Adding documentation to this PR
Next PR will be workflows

[RUN with test project types and models](https://github.com/orgs/trezor/projects/95/views/1?visibleFields=%5B%22Title%22%2C%22Assignees%22%2C%22Status%22%2C186733455%2C186733454%2C186733468%2C186733469%2C186733482%5D)
[RUN with more tests and some fails](https://github.com/orgs/trezor/projects/93/views/1?visibleFields=%5B%22Title%22%2C%22Assignees%22%2C%22Status%22%2C186487216%2C186487217%2C186487218%2C186487219%5D&groupedBy%5BcolumnId%5D=186487216&sortedBy%5Bdirection%5D=asc&sortedBy%5BcolumnId%5D=186487218)

🔍🖥️ **Suite web test results:** [View in Currents](https://app.currents.dev/projects/Og0NOQ/runs?branch=feat-pw-github-reporter&lookback=7)

🔍🖥️ **Suite desktop test results:** [View in Currents](https://app.currents.dev/projects/4ytF0E/runs?branch=feat-pw-github-reporter&lookback=7)


Diff:
diff --git a/docs/tests/e2e-playwright-github-reporter.md b/docs/tests/e2e-playwright-github-reporter.md
new file mode 100644
index 000000000000..6ad5b4f27c65
--- /dev/null
+++ b/docs/tests/e2e-playwright-github-reporter.md
@@ -0,0 +1,115 @@
+# GitHub Test Reporter
+
+The GitHub Test Reporter is a unified test documentation and reporting framework designed to create a single source of truth for both automated and manual tests in our repository. This framework uses structured test annotations to document tests directly in the codebase and automatically generates GitHub projects during release builds for efficient QA workflows.
+
+## Purpose
+
+The primary goals of this reporter are to:
+
+1. **Unify Test Documentation** - Keep all test cases (both automated and manual) in one place within the repository
+2. **Streamline QA Regression** - Generate release-specific project boards with all tests as draft issues
+
+When executed during release builds, the reporter creates a new GitHub project populated with draft issues representing each test. These issues contain all relevant test metadata and results, providing QA teams with a comprehensive dashboard for regression testing and quality assurance.
+
+## Overview
+
+The framework consists of three main components:
+
+1. **Test Annotations** - Define metadata about tests
+2. **TestReportProvider** - Extracts and formats test metadata
+3. **GitHub Reporter** - Creates release build project and populates it with GitHub issues with test results
+
+The reporter automatically creates draft issues in a GitHub project board when tests run, complete with test metadata, status, and other customizable fields.
+
+## Annotation Types
+
+Test annotations are defined in `testAnnotations.ts`, all of them are optional and include:
+
+| Annotation Type | Description                                            |
+| --------------- | ------------------------------------------------------ |
+| `testCase`      | The title of the test                                  |
+| `prerequisites` | List of requirements before running the test           |
+| `steps`         | List of steps to execute the test                      |
+| `category`      | Test category (e.g., Onboarding, Security, Wallets)    |
+| `priority`      | Test priority (Critical, High, Medium, Low)            |
+| `stream`        | Team assignment (Trends, Foundation, Engagement, etc.) |
+| `deviceModel`   | Target device model (T1B1, T2T1, etc.)                 |
+
+## Project Fields
+
+The GitHub project is configured with fields that correspond to test metadata:
+
+-   **Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc.)
+-   **Stream** - Team assignment
+-   **Test Run** - Whether the test is manual or automated (and which platform)
+-   **Priority** - Test importance
+-   **Device Model** - Target device for testing
+-   **Comment** - Additional notes
+
+## How to Use
+
+### Adding Annotations to Tests
+
+```typescript
+test.describe('Test suite name', { tag: ['@group=manual'] }, () => {
+    test(
+        'My test case title',
+        {
+            annotation: createTestAnnotation({
+                testCase: 'Verify user can login with correct credentials',
+                prerequisites: ['User has an existing account', 'Application is at login screen'],
+                steps: [
+                    'Enter valid username',
+                    'Enter valid password',
+                    'Click login button',
+                    'Verify dashboard is displayed',
+                ],
+                category: TestCategory.Security,
+                priority: TestPriority.High,
+                stream: TestStream.Foundation,
+            }),
+        },
+
+        async ({ page }) => {},
+    );
+});
+```
+
+### Configuration
+
+The reporter is configured with environment variables:
+
+-   `GITHUB_TOKEN` - Required for API access
+-   `PROJECT_NAME` - Optional name for the GitHub project (defaults to "Test Results 35")
+-   `VERBOSE` - Set to control logging detail
+
+### Local run
+
+The reporter can be run even from local environment for troubleshooting and development purposes.
+
+-   `yarn test:e2e:web:report`
+-   `yarn test:e2e:desktop:report`
+
+## Implementation Notes
+
+### Retry Strategy
+
+The GitHub Test Reporter implements a retry mechanism to handle network issues or temporary API failures. It uses the `scheduleAction` utility. Each API operation will be attempted up to 3 times with a 500ms delay between attempts.
+The current implementation does not have built-in deduplication logic for test issues.
+
+### Extending the Framework
+
+To add new annotation types:
+
+1. Add a new enum value in `TestAnnotationType`
+2. Create a new annotation definition object
+3. Add it to the appropriate arrays:
+    - `annotationsForBodyDescription` - For issue body content
+    - `annotationsForProjectFields` - For project board fields
+    - `annotationsAddedToTest` - For annotations that can be set on tests
+4. Update `TestReportProvider`
+    - Add new getter for the new annotation
+    - adds its default to class property defaults
+    - update constant validGetterKeys in getterByKey
+
+To add or change value of annotation, simply change or add new enum value in corresponding object in `testAnnotations.ts`
diff --git a/packages/suite-desktop-core/.example.env b/packages/suite-desktop-core/.example.env
index 8e8f184b9bd6..5ef3c17c5404 100644
--- a/packages/suite-desktop-core/.example.env
+++ b/packages/suite-desktop-core/.example.env
@@ -3,4 +3,7 @@
 PASSPHRASE=
 
 # Custom Test timeout override, for convenient test development (optional)
-TEST_TIMEOUT_OVERRIDE=
\ No newline at end of file
+TEST_TIMEOUT_OVERRIDE=
+
+# Token for GitHub Reporter local runs, used only for testing of reporter itself (optional)
+# GITHUB_TOKEN=
\ No newline at end of file
diff --git a/packages/suite-desktop-core/e2e/support/annotations.ts b/packages/suite-desktop-core/e2e/support/annotations.ts
index 79b440c50f9a..951a8a5a7c2d 100644
--- a/packages/suite-desktop-core/e2e/support/annotations.ts
+++ b/packages/suite-desktop-core/e2e/support/annotations.ts
@@ -1,8 +1,18 @@
+import { TestDetailsAnnotation } from '@playwright/test';
+import { TestCase } from '@playwright/test/reporter';
+
+import { capitalizeFirstLetter } from '@trezor/utils';
+
 import {
+    DeviceModel,
     TestAnnotationType,
     TestCategory,
     TestPriority,
+    TestStatus,
     TestStream,
+    annotationsAddedToTest,
+    annotationsForBodyDescription,
+    annotationsForProjectFields,
 } from './enums/testAnnotations';
 
 type TestMetadataInput = {
@@ -14,53 +24,173 @@ type TestMetadataInput = {
     stream?: TestStream;
 };
 
-export const formatTestSteps = (steps: string[]): string =>
+const formatList = (steps: string[]): string =>
     steps.map((step, index) => `${index + 1}. ${step}`).join('\n');
 
-export const createTestAnnotation = (metadata: TestMetadataInput) => {
+// Loops thru params and adds these metadata to the test as annotation, used in test files
+export const createTestAnnotation = (metadata: TestMetadataInput): TestDetailsAnnotation[] => {
     const formattedAnnotations = [];
 
-    if (metadata.testCase) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.TestCase,
-            description: metadata.testCase,
-        });
+    for (const [key, value] of Object.entries(metadata)) {
+        const annotation = annotationsAddedToTest.find(a => a.key === key);
+        if (!value || !annotation?.annotationType) {
+            continue;
+        }
+
+        const type = annotation.annotationType.toString();
+        if (annotation.needsFormatting) {
+            formattedAnnotations.push({ type, description: formatList(value as string[]) });
+        } else {
+            formattedAnnotations.push({ type, description: value as string });
+        }
+    }
+
+    return formattedAnnotations;
+};
+
+// Class used by our GitHub Reporter to extract metadata from the test and its run
+export class TestReportProvider {
+    private readonly test: TestCase;
+    private readonly annotationMap: Map<string, string>;
+    private readonly defaults = {
+        prerequisites: 'No prerequisites defined',
+        steps: 'No steps defined',
+        category: TestCategory.NotCategorized,
+        priority: TestPriority.Medium,
+        stream: TestStream.NotDefined,
+        deviceModel: DeviceModel.Unknown,
+    };
+
+    constructor(test: TestCase) {
+        this.test = test;
+        this.annotationMap = new Map();
+
+        for (const annotation of test.annotations) {
+            if (!annotation.description) {
+                continue;
+            }
+            this.annotationMap.set(annotation.type, annotation.description);
+        }
     }
 
-    if (metadata.prerequisites) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Prerequisites,
-            description: formatTestSteps(metadata.prerequisites),
-        });
+    private getAnnotation(type: string, defaultValue: string): string {
+        return this.annotationMap.has(type) ? this.annotationMap.get(type)! : defaultValue;
     }
 
-    if (metadata.steps) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Steps,
-            description: formatTestSteps(metadata.steps),
-        });
+    get testCase(): string {
+        return this.getAnnotation(TestAnnotationType.TestCase, this.test.title);
     }
+    get status(): string {
+        // This condition covers manual and automated tests that are skipped
+        if (this.test.outcome() === 'skipped') {
+            return TestStatus.Todo;
+        }
 
-    if (metadata.category) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Category,
-            description: metadata.category,
-        });
+        if (this.test.ok()) {
+            return TestStatus.AutoPass;
+        }
+
+        if (!this.test.ok()) {
+            return TestStatus.AutoFail;
+        }
+
+        return TestStatus.Todo;
     }
 
-    if (metadata.priority) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Priority,
-            description: metadata.priority,
-        });
+    get prerequisites(): string {
+        return this.getAnnotation(TestAnnotationType.Prerequisites, this.defaults.prerequisites);
     }
 
-    if (metadata.stream) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Stream,
-            description: metadata.stream,
-        });
+    get steps(): string {
+        return this.getAnnotation(TestAnnotationType.Steps, this.defaults.steps);
     }
 
-    return formattedAnnotations;
-};
+    get category(): string {
+        return this.getAnnotation(TestAnnotationType.Category, this.defaults.category);
+    }
+
+    get priority(): string {
+        return this.getAnnotation(TestAnnotationType.Priority, this.defaults.priority);
+    }
+
+    get stream(): string {
+        return this.getAnnotation(TestAnnotationType.Stream, this.defaults.stream);
+    }
+
+    get testRun(): string {
+        if (this.isManual) {
+            return 'Manual';
+        } else {
+            // Web or Desktop
+            const projectType = this.test.parent.project()?.name;
+
+            return capitalizeFirstLetter(projectType ?? 'Automated');
+        }
+    }
+
+    get deviceModel(): string {
+        return this.getAnnotation(TestAnnotationType.DeviceModel, this.defaults.deviceModel);
+    }
+
+    get comment(): string {
+        return '';
+    }
+
+    get rawAnnotations(): Array<{ type: string; description?: string }> {
+        return this.test.annotations;
+    }
+
+    get isManual(): boolean {
+        return this.test.tags.some(tag => tag.startsWith('@group=manual'));
+    }
+
+    get bodyDescription(): string {
+        const sections = [];
+
+        if (this.isManual) {
+            for (const annotation of annotationsForBodyDescription) {
+                const value = this.getterByKey(annotation.key);
+                sections.push(`## ${annotation.name}\n${value}`);
+            }
+        } else {
+            sections.push('## Automated Test');
+        }
+
+        return sections.join('\n---\n');
+    }
+
+    get projectValues(): Array<{ name: string; value: string }> {
+        return annotationsForProjectFields.map(field => ({
+            name: field.name,
+            value: this.getterByKey(field.key),
+        }));
+    }
+
+    // This method allow us to loop thru array of keys and get the value from the getter
+    // That way `bodyDescription` and `projectValues` can be generated dynamically
+    // and rely on annotations objects (ex: 'annotationsForBodyDescription') as single source definitions
+    getterByKey(key: string): string {
+        // This is the downside, we need to record of all our annotation getters here
+        const getters: Record<string, () => string> = {
+            testCase: () => this.testCase,
+            prerequisites: () => this.prerequisites,
+            steps: () => this.steps,
+            category: () => this.category,
+            priority: () => this.priority,
+            stream: () => this.stream,
+            status: () => this.status,
+            testRun: () => this.testRun,
+            deviceModel: () => this.deviceModel,
+            comment: () => this.comment,
+        };
+
+        const getter = getters[key];
+        if (!getter) {
+            throw new Error(
+                `The key '${key}' does not have corresponding getter on TestReportProvider class.`,
+            );
+        }
+
+        return getter();
+    }
+}
diff --git a/packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts b/packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts
index 9f528b82b669..6f53dd5509e7 100644
--- a/packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts
+++ b/packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts
@@ -1,14 +1,16 @@
+// Types of annotations that can be defined in the test file
 export enum TestAnnotationType {
-    TestCase = 'testcase',
+    TestCase = 'testCase',
     Prerequisites = 'prerequisites',
     Steps = 'steps',
     Category = 'category',
     Priority = 'priority',
     Stream = 'stream',
-    OsMatrix = 'os_matrix',
-    DeviceMatrix = 'device_matrix',
+    OsMatrix = 'osMatrix',
+    DeviceModel = 'deviceModel',
 }
 
+// Enums for TestAnnotationType
 export enum TestCategory {
     Onboarding = 'Onboarding',
     UriLinkHandler = 'URI link handler',
@@ -36,6 +38,7 @@ export enum TestCategory {
     Staking = 'Staking',
     Solana = 'Solana',
     Engagement = 'Engagement',
+    NotCategorized = 'Not Categorized',
 }
 
 export enum TestPriority {
@@ -46,9 +49,157 @@ export enum TestPriority {
     AsNecessary = 'As necessary',
 }
 
+export const TestPriorityColors: Record<TestPriority, string> = {
+    [TestPriority.Critical]: 'RED',
+    [TestPriority.High]: 'ORANGE',
+    [TestPriority.Medium]: 'YELLOW',
+    [TestPriority.Low]: 'GREEN',
+    [TestPriority.AsNecessary]: 'GRAY',
+};
+
 export enum TestStream {
     Trends = 'Trends',
     Foundation = 'Foundation',
     Engagement = 'Engagement',
     Firmware = 'Firmware',
+    NotDefined = 'Not Defined',
+}
+
+export enum TestStatus {
+    Todo = 'Todo',
+    InProgress = 'In Progress',
+    DonePass = 'Done PASS',
+    DoneFail = 'Done FAIL',
+    AutoPass = 'Auto PASS',
+    AutoFail = 'Auto FAIL',
+    Skipped = 'Skipped',
+}
+
+export const TestStatusColors: Record<TestStatus, string> = {
+    [TestStatus.Todo]: 'BLUE',
+    [TestStatus.InProgress]: 'YELLOW',
+    [TestStatus.DonePass]: 'GREEN',
+    [TestStatus.DoneFail]: 'RED',
+    [TestStatus.AutoPass]: 'GREEN',
+    [TestStatus.AutoFail]: 'RED',
+    [TestStatus.Skipped]: 'GRAY',
+};
+
+export enum DeviceModel {
+    T1B1 = 'T1B1',
+    T2T1 = 'T2T1',
+    T2B1 = 'T2B1',
+    T3T1 = 'T3T1',
+    Unknown = 'Unknown',
 }
+
+// Basic annotation definition
+export interface BaseAnnotation {
+    name: string;
+    key: string;
+    valueType: 'TEXT' | 'SINGLE_SELECT';
+    valueOptions?: string[];
+    optionsColors?: Record<string, string>;
+    annotationType?: TestAnnotationType;
+    needsFormatting?: boolean;
+}
+
+// Individual annotation definitions
+export const testCaseAnnotation: BaseAnnotation = {
+    name: 'Test Case',
+    key: 'testCase',
+    annotationType: TestAnnotationType.TestCase,
+    valueType: 'TEXT',
+};
+
+export const prerequisitesAnnotation: BaseAnnotation = {
+    name: 'Prerequisites',
+    key: 'prerequisites',
+    annotationType: TestAnnotationType.Prerequisites,
+    valueType: 'TEXT',
+    needsFormatting: true,
+};
+
+export const stepsAnnotation: BaseAnnotation = {
+    name: 'Steps',
+    key: 'steps',
+    annotationType: TestAnnotationType.Steps,
+    valueType: 'TEXT',
+    needsFormatting: true,
+};
+
+export const statusAnnotation: BaseAnnotation = {
+    name: 'Status',
+    key: 'status',
+    valueType: 'SINGLE_SELECT',
+    valueOptions: Object.values(TestStatus),
+    optionsColors: TestStatusColors,
+};
+
+export const streamAnnotation: BaseAnnotation = {
+    name: 'Stream',
+    key: 'stream',
+    annotationType: TestAnnotationType.Stream,
+    valueType: 'SINGLE_SELECT',
+    valueOptions: Object.values(TestStream),
+};
+
+export const testRunAnnotation: BaseAnnotation = {
+    name: 'Test Run',
+    key: 'testRun',
+    valueType: 'TEXT',
+};
+
+export const categoryAnnotation: BaseAnnotation = {
+    name: 'Category',
+    key: 'category',
+    annotationType: TestAnnotationType.Category,
+    valueType: 'SINGLE_SELECT',
+    valueOptions: Object.values(TestCategory),
+};
+
+export const priorityAnnotation: BaseAnnotation = {
+    name: 'Priority',
+    key: 'priority',
+    annotationType: TestAnnotationType.Priority,
+    valueType: 'SINGLE_SELECT',
+    valueOptions: Object.values(TestPriority),
+    optionsColors: TestPriorityColors,
+};
+
+export const deviceModelAnnotation: BaseAnnotation = {
+    name: 'Device Model',
+    key: 'deviceModel',
+    annotationType: TestAnnotationType.DeviceModel,
+    valueType: 'SINGLE_SELECT',
+    valueOptions: Object.values(DeviceModel),
+};
+
+export const commentAnnotation: BaseAnnotation = {
+    name: 'Comment',
+    key: 'comment',
+    valueType: 'TEXT',
+};
+
+// Defines which annotations are used in the body description in GitHub Issue
+export const annotationsForBodyDescription = [prerequisitesAnnotation, stepsAnnotation];
+
+// Defines which annotations are used in the project fields in GitHub Issue
+export const annotationsForProjectFields = [
+    statusAnnotation,
+    streamAnnotation,
+    testRunAnnotation,
+    priorityAnnotation,
+    deviceModelAnnotation,
+    commentAnnotation,
+];
+
+// Defines which annotations are set for a test case in Playwright
+export const annotationsAddedToTest = [
+    testCaseAnnotation,
+    prerequisitesAnnotation,
+    stepsAnnotation,
+    streamAnnotation,
+    categoryAnnotation,
+    priorityAnnotation,
+];
diff --git a/packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts b/packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
new file mode 100644
index 000000000000..44ea1b30e677
--- /dev/null
+++ b/packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
@@ -0,0 +1,165 @@
+import type { Octokit } from '@octokit/rest';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { ProjectRequests } from './projectRequests';
+import { LoggingFunctions } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 37'; // TODO: Get this from CI, probably build name
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+export class GitHubProject {
+    private _projectId: string | undefined;
+    private graphQLClient: ProjectRequests;
+
+    constructor(
+        octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {
+        this.graphQLClient = new ProjectRequests(octokit, logger);
+    }
+
+    get id(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    async init(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.logger.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.logger.log(
+                    `Using created project: ${createdProject.title} (${createdProject.id})`,
+                );
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logger.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
+        try {
+            const projects = await scheduleAction(
+                () => this.graphQLClient.getProjectFromOrganization(ORGANIZATION, PROJECT_NAME),
+                RETRY_CONF,
+            );
+
+            const matchingProject = projects.find((project: any) => project.title === PROJECT_NAME);
+
+            if (!matchingProject) {
+                return null;
+            }
+
+            const areThereDuplicates =
+                projects.filter((project: any) => project.title === PROJECT_NAME).length > 1;
+            if (areThereDuplicates) {
+                this.logger.log(
+                    `Warning: Multiple projects found with title "${PROJECT_NAME}". Using the first one with number ${matchingProject.number}.`,
+                );
+            }
+
+            return matchingProject;
+        } catch (error) {
+            this.logger.logError('Failed to find project.');
+            throw error;
+        }
+    }
+
+    async createProject(desiredFields: Array<BaseAnnotation>): Promise<void> {
+        const projectId = await this.graphQLClient.createProject(ORG_ID, QA_TEAM_ID, PROJECT_NAME);
+
+        // Get default STATUS field that was automatically created.
+        const existingFields = await scheduleAction(
+            () => this.graphQLClient.getProjectFields(projectId),
+            RETRY_CONF,
+        );
+        const existingStatusField = existingFields.find(f => f.name === 'Status');
+
+        for (const desiredField of desiredFields) {
+            // Update STATUS field with new options
+            if (desiredField.name === 'Status' && existingStatusField) {
+                this.logger.log('Status field already exists, updating options...');
+                await this.updateProjectFieldOptions(existingStatusField.id, desiredField);
+                continue;
+            }
+
+            try {
+                const desiredOptions =
+                    desiredField.valueType === 'SINGLE_SELECT' && desiredField.valueOptions
+                        ? desiredField.valueOptions.map(value => {
+                              const color = desiredField.optionsColors?.[value] || 'GRAY';
+
+                              return { value, color };
+                          })
+                        : undefined;
+
+                await scheduleAction(
+                    () =>
+                        this.graphQLClient.createProjectField(
+                            projectId,
+                            desiredField.name,
+                            desiredField.valueType,
+                            desiredOptions,
+                        ),
+                    RETRY_CONF,
+                );
+            } catch (error) {
+                this.logger.logError(`Error creating field "${desiredField.name}".`);
+                throw error;
+            }
+        }
+    }
+
+    async updateProjectFieldOptions(fieldId: string, desiredField: BaseAnnotation): Promise<void> {
+        this.logger.log(`Updating field options for "${desiredField.name}" (${fieldId})`);
+
+        try {
+            const desiredOptions = desiredField.valueOptions!.map(value => {
+                const color = desiredField.optionsColors?.[value] || 'GRAY';
+
+                return { value, color };
+            });
+
+            await scheduleAction(
+                () => this.graphQLClient.updateFieldOptions(fieldId, desiredOptions),
+                RETRY_CONF,
+            );
+            this.logger.log(`Successfully updated options for field "${desiredField.name}"`);
+        } catch (error) {
+            this.logger.logError(`Failed to update options for field "${desiredField.name}".`);
+            throw error;
+        }
+    }
+}
diff --git a/packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts b/packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
new file mode 100644
index 000000000000..a4e0b8a88a41
--- /dev/null
+++ b/packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
@@ -0,0 +1,279 @@
+import type { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubProject } from './gitHubProject';
+import { IssueRequests } from './issueRequests';
+import { LoggingFunctions, ProjectField } from './types';
+
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+enum InitializationState {
+    NOT_STARTED = 'NOT_STARTED',
+    IN_PROGRESS = 'IN_PROGRESS',
+    COMPLETED = 'COMPLETED',
+    FAILED = 'FAILED',
+}
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _issueRequests: IssueRequests | null = null;
+    private _gitHubProject: GitHubProject | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initState: InitializationState = InitializationState.NOT_STARTED;
+
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get issueRequests(): IssueRequests {
+        if (!this._issueRequests) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._issueRequests;
+    }
+
+    private get gitHubProject(): GitHubProject {
+        if (!this._gitHubProject) {
+            throw new Error('GitHub project is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._gitHubProject;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        this.initState = InitializationState.IN_PROGRESS;
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._issueRequests = new IssueRequests(this.octokit);
+                this._gitHubProject = new GitHubProject(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.initState = InitializationState.FAILED;
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.gitHubProject.init();
+            this.initState = InitializationState.COMPLETED;
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                await this.waitForOnBeginInit();
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
+                        this.log(`Creating GitHub draft issue for test "${test.title}"...`);
+
+                        return this.issueRequests.createDraftIssueInProject(
+                            this.gitHubProject.id,
+                            report.testCase,
+                            report.bodyDescription,
+                        );
+                    }, RETRY_CONF);
+                    this.log(`[${issueNodeId}] Successfully created issue "${test.title}"`);
+
+                    const fields = await scheduleAction(() => this.getProjectFields(), RETRY_CONF);
+
+                    for (const { name, value } of report.projectValues) {
+                        const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(
+                            fields,
+                            name,
+                            value,
+                        );
+                        await scheduleAction(() => {
+                            this.log(`[${issueNodeId}] Updating field ${name}:"${value}"...`);
+
+                            return this.issueRequests.setItemValue(
+                                this.gitHubProject.id,
+                                issueNodeId,
+                                fieldId,
+                                valueOrOptionId,
+                            );
+                        }, RETRY_CONF);
+                        this.log(`[${issueNodeId}] Successfully updated field ${name}:"${value}"`);
+                    }
+
+                    this.log(
+                        `[${issueNodeId}] Successfully recorded test result for "${test.title}"`,
+                    );
+                } catch (error) {
+                    this.logError(`Failed to process test end for "${test.title}":`, error);
+                    // Non-Critical error, no need to rethrow
+                }
+            })(),
+        );
+    }
+
+    // Finalizes reporting when all tests are complete, waits for pending operations to finish
+    // eslint-disable-next-line require-await
+    async onEnd() {
+        this.log('All tests completed, waiting for pending operations...');
+
+        if (this.pendingOperations.length > 0) {
+            this.log(`Waiting for ${this.pendingOperations.length} pending operations to complete`);
+
+            return Promise.allSettled(this.pendingOperations)
+                .then(results => {
+                    const failed = results.filter(r => r.status === 'rejected').length;
+                    if (failed > 0) {
+                        this.logError(`${failed} operations failed`);
+                    } else {
+                        this.log('All operations completed successfully');
+                    }
+                })
+                .finally(() => {
+                    this.log('GitHub reporter finished');
+                });
+        } else {
+            this.log('No pending operations, GitHub reporter finished');
+
+            return Promise.resolve();
+        }
+    }
+
+    private async getProjectFields(): Promise<ProjectField[]> {
+        if (this.cachedFields) {
+            this.log('Using cached project fields');
+
+            return this.cachedFields;
+        }
+
+        this.log(`Fetching fields for project ${this.gitHubProject.id}...`);
+        const fields = await this.issueRequests.getProjectFields(this.gitHubProject.id);
+        this.log(`Successfully retrieved fields for project ${this.gitHubProject.id}`);
+        this.cachedFields = fields;
+
+        return fields;
+    }
+
+    private resolveFieldAndValue(
+        fields: ProjectField[],
+        name: string,
+        value: string,
+    ): { fieldId: string; valueOrOptionId: string } {
+        const field = fields.find(f => f.name === name);
+
+        if (!field) {
+            throw new Error(
+                `Field "${name}" not found in project fields: \n ${JSON.stringify(fields, null, 2)}`,
+            );
+        }
+
+        if (field.dataType === 'SINGLE_SELECT' && field.options) {
+            const option = field.options.find(opt => opt.name === value);
+            if (!option) {
+                throw new Error(
+                    `Value "${value}" not found in field "${name}". Options: \n ${JSON.stringify(field.options, null, 2)}`,
+                );
+            }
+
+            return {
+                fieldId: field.id,
+                valueOrOptionId: `{ singleSelectOptionId: "${option.id}" }`,
+            };
+        }
+
+        // Currently we support only SINGLE_SELECT and TEXT fields
+        return { fieldId: field.id, valueOrOptionId: `{ text: "${value}" }` };
+    }
+
+    private async waitForOnBeginInit(): Promise<void> {
+        if (this.initState === InitializationState.COMPLETED) {
+            return;
+        }
+
+        if (this.initState === InitializationState.FAILED) {
+            throw new Error('GitHub reporter onBegin initialization failed previously');
+        }
+
+        if (this.initState === InitializationState.NOT_STARTED) {
+            // Wait until state changes from NOT_STARTED to something else
+            await new Promise<void>((resolve, reject) => {
+                const checkInterval = setInterval(() => {
+                    if (this.initState !== InitializationState.NOT_STARTED) {
+                        clearInterval(checkInterval);
+                        resolve();
+                    }
+                }, 100);
+
+                setTimeout(() => {
+                    clearInterval(checkInterval);
+                    reject(new Error('Timed out waiting for onBegin initialization to start'));
+                }, 30_000);
+            });
+
+            // Now state should be changed, call ensureInitialized again to handle the new state
+            return this.waitForOnBeginInit();
+        }
+
+        if (this.initState === InitializationState.IN_PROGRESS && this.initializationPromise) {
+            await this.initializationPromise;
+        }
+    }
+}
+
+// eslint-disable-next-line import/no-default-export
+export default GitHubReporter;
diff --git a/packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts b/packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
new file mode 100644
index 000000000000..9e6a025cc805
--- /dev/null
+++ b/packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
@@ -0,0 +1,95 @@
+import type { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    private escapeGraphQLString(input: string): string {
+        return input.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${this.escapeGraphQLString(title)}"
+              body: "${this.escapeGraphQLString(body)}"
+            }
+          ) {
+            projectItem {
+              id
+            }
+          }
+        }
+    `;
+
+        const response = await this.octokit.graphql<AddDraftIssueResponse>(mutation);
+        const issueId = response.addProjectV2DraftIssue.projectItem.id;
+
+        return issueId;
+    }
+}
diff --git a/packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts b/packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
new file mode 100644
index 000000000000..26bfa9fe63f1
--- /dev/null
+++ b/packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
@@ -0,0 +1,190 @@
+import type { Octokit } from '@octokit/rest';
+
+import {
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+} from './types';
+
+export async function getProjectFields(
+    octokit: Octokit,
+    projectId: string,
+): Promise<ProjectField[]> {
+    const query = `
+        query {
+          node(id: "${projectId}") {
+            ... on ProjectV2 {
+              fields(first: 20) {
+                nodes {
+                  ... on ProjectV2FieldCommon {
+                    id
+                    name
+                    dataType
+                  }
+                  ... on ProjectV2SingleSelectField {
+                    id
+                    name
+                    dataType
+                    options {
+                      id
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    `;
+
+    const response = await octokit.graphql<ProjectFieldsResponse>(query);
+
+    return response.node.fields.nodes;
+}
+
+export class ProjectRequests {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
+
+    async createProjectField(
+        projectId: string,
+        fieldName: string,
+        fieldType: string,
+        options?: { value: string; color: string }[],
+    ): Promise<string> {
+        const isSelectField = fieldType === 'SINGLE_SELECT';
+        let optionsString = '';
+        this.logger.log(
+            `Creating ${fieldType} field "${fieldName}" in project ${projectId}${
+                isSelectField ? ` with ${options!.length} options` : ''
+            }...`,
+        );
+
+        if (isSelectField && options) {
+            optionsString = options
+                .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+                .join(',\n');
+        }
+
+        const mutation = `
+      mutation {
+        createProjectV2Field(
+        input: {
+          projectId: "${projectId}"
+          name: "${fieldName}"
+          dataType: ${fieldType}
+          ${isSelectField ? `singleSelectOptions: [${optionsString}]` : ''}
+        }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+        } 
+      }`;
+
+        const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
+        const fieldId = response.createProjectV2Field.projectV2Field.id;
+        this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
+
+        return fieldId;
+    }
+
+    async getProjectFromOrganization(
+        organization: string,
+        projectName: string,
+    ): Promise<Project[]> {
+        this.logger.log(`Fetching projects for organization: ${organization}`);
+
+        const query = `
+            query {
+              organization(login: "${organization}") {
+                projectsV2(first: 30, query: "${projectName}", orderBy: {field: NUMBER, direction: ASC}) {
+                  nodes {
+                    id
+                    title
+                    number
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponse>(query);
+        this.logger.log('Successfully retrieved organization projects');
+
+        return response.organization.projectsV2.nodes;
+    }
+
+    async getProjectFields(projectId: string): Promise<ProjectField[]> {
+        this.logger.log(`Fetching fields for project ${projectId}...`);
+        const response = await getProjectFields(this.octokit, projectId);
+        this.logger.log(`Successfully retrieved fields for project ${projectId}`);
+
+        return response;
+    }
+
+    async updateFieldOptions(
+        fieldId: string,
+        options: { value: string; color: string }[],
+    ): Promise<void> {
+        this.logger.log(`Updating all options for field ${fieldId}`);
+
+        const optionsString = options
+            .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+            .join(',\n');
+
+        const mutation = `
+        mutation {
+          updateProjectV2Field(
+          input: {
+            fieldId: "${fieldId}"
+            singleSelectOptions: [
+              ${optionsString}
+            ]
+          }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+          } 
+        }
+      `;
+
+        await this.octokit.graphql(mutation);
+        this.logger.log(`Successfully replaced all options for field ${fieldId}`);
+    }
+}
diff --git a/packages/suite-desktop-core/e2e/support/reporters/types.ts b/packages/suite-desktop-core/e2e/support/reporters/types.ts
new file mode 100644
index 000000000000..61a05d790c4a
--- /dev/null
+++ b/packages/suite-desktop-core/e2e/support/reporters/types.ts
@@ -0,0 +1,86 @@
+export interface LoggingFunctions {
+    log: (...args: any[]) => void;
+    logError: (...args: any[]) => void;
+    logResponse: (label: string, response: any) => void;
+}
+
+export interface Project {
+    id: string;
+    title: string;
+    number: number;
+}
+
+// GraphQL Response Types
+export interface CreateProjectMutation {
+    createProjectV2: {
+        projectV2: {
+            id: string;
+        };
+    };
+}
+
+export interface ProjectQueryResponse {
+    organization: {
+        projectsV2: {
+            nodes: Project[];
+        };
+    };
+}
+
+export interface AddIssueToProjectResponse {
+    addProjectV2ItemById: {
+        item: {
+            id: string;
+        };
+    };
+}
+
+export interface FieldOption {
+    id: string;
+    name: string;
+}
+
+export interface ProjectField {
+    id: string;
+    name: string;
+    dataType?: string;
+    options?: Array<FieldOption>;
+}
+
+export interface ProjectFieldsResponse {
+    node: {
+        fields: {
+            nodes: ProjectField[];
+        };
+    };
+}
+
+export interface UpdateProjectItemFieldResponse {
+    updateProjectV2ItemFieldValue: {
+        projectV2Item: {
+            id: string;
+        };
+    };
+}
+
+export interface CreateFieldResponse {
+    createProjectV2Field: {
+        projectV2Field: {
+            id: string;
+        };
+    };
+}
+
+export interface AddDraftIssueResponse {
+    addProjectV2DraftIssue: {
+        projectItem: {
+            id: string;
+            content: {
+                id: string;
+                title: string;
+            };
+        };
+    };
+}
+
+export type ValueOrOptionId = string | { optionId: string };
diff --git a/packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts b/packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts
index 1182c160f54f..9bc4d88613c2 100644
--- a/packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts
+++ b/packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts
@@ -6,6 +6,7 @@ import { Model, SetupEmu, StartEmu, TrezorUserEnvLinkClass } from '@trezor/trezo
 import { TrezorUserEnvLinkProxy, getUrl, getVideoPath, isDesktopProject } from '../common';
 import { LaunchSuiteParams, Suite, launchSuite } from '../electron';
 import { enhancePage } from './enhancePage';
+import { TestAnnotationType } from '../enums/testAnnotations';
 
 type StartEmuModelRequired = StartEmu & { model: Model };
 
@@ -98,6 +99,11 @@ const trezorEnvSetup = async (
     await TrezorUserEnvLinkProxy.logTestDetails(
         ` - - - STARTING TEST ${testInfo.titlePath.join(' - ')}`,
     );
+    // This test annotation is used by our GitHub reporter
+    testInfo.annotations.push({
+        type: TestAnnotationType.DeviceModel,
+        description: emulatorStartConf.model,
+    });
     // We cannot rely on that previous teardown was done correctly
     await TrezorUserEnvLinkProxy.stopBridge();
     await TrezorUserEnvLinkProxy.stopEmu();
diff --git a/packages/suite-desktop-core/package.json b/packages/suite-desktop-core/package.json
index f13a356bc711..dc0d505b873e 100644
--- a/packages/suite-desktop-core/package.json
+++ b/packages/suite-desktop-core/package.json
@@ -15,6 +15,8 @@
         "test:orchestrated:e2e:desktop": "yarn xvfb-maybe -- pwc-p --config=./e2e/playwright.config.ts --project=desktop",
         "test:e2e:web": "yarn xvfb-maybe -- playwright test --config=./e2e/playwright.config.ts --project=web",
         "test:e2e:desktop": "yarn xvfb-maybe -- playwright test --config=./e2e/playwright.config.ts --project=desktop",
+        "test:e2e:web:report": "yarn xvfb-maybe -- playwright test --config=./e2e/playwright.config.ts --project=web --reporter=./e2e/support/reporters/gitHubReporter.ts",
+        "test:e2e:desktop:report": "yarn xvfb-maybe -- playwright test --config=./e2e/playwright.config.ts --project=desktop --reporter=./e2e/support/reporters/gitHubReporter.ts",
         "test:e2e:update-snapshots": "yarn xvfb-maybe -- playwright test --config=./e2e/playwright.config.ts --grep @snapshot --update-snapshots"
     },
     "dependencies": {
@@ -53,6 +55,7 @@
         "@currents/playwright": "^1.11.3",
         "@electron/fuses": "^1.8.0",
         "@electron/notarize": "2.5.0",
+        "@octokit/rest": "^21.1.1",
         "@playwright/browser-chromium": "^1.51.0",
         "@playwright/browser-firefox": "^1.51.0",
         "@playwright/browser-webkit": "^1.51.0",
diff --git a/scripts/list-outdated-dependencies/qa-dependencies.txt b/scripts/list-outdated-dependencies/qa-dependencies.txt
index 0e31eed51f27..64bd4cf75e52 100644
--- a/scripts/list-outdated-dependencies/qa-dependencies.txt
+++ b/scripts/list-outdated-dependencies/qa-dependencies.txt
@@ -1,5 +1,6 @@
 @currents/cmd
 @currents/playwright
+@octokit/rest
 @playwright/browser-chromium
 @playwright/browser-firefox
 @playwright/browser-webkit
diff --git a/yarn.lock b/yarn.lock
index 526879332948..1154fddcf9b7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5384,6 +5384,130 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@octokit/auth-token@npm:^5.0.0":
+  version: 5.1.2
+  resolution: "@octokit/auth-token@npm:5.1.2"
+  checksum: 10/53636ea9dbf77d491ce387d36f75b2c9f76a8f71de0d892e08999e6ef3382a151bd52faf5ad9711b9ade6c19cfe71d4c69cfd4a16594858299201cef93412b0f
+  languageName: node
+  linkType: hard
+
+"@octokit/core@npm:^6.1.4":
+  version: 6.1.4
+  resolution: "@octokit/core@npm:6.1.4"
+  dependencies:
+    "@octokit/auth-token": "npm:^5.0.0"
+    "@octokit/graphql": "npm:^8.1.2"
+    "@octokit/request": "npm:^9.2.1"
+    "@octokit/request-error": "npm:^6.1.7"
+    "@octokit/types": "npm:^13.6.2"
+    before-after-hook: "npm:^3.0.2"
+    universal-user-agent: "npm:^7.0.0"
+  checksum: 10/e6ca903ce037a854c86da93ecf4d12315963745cc3580804cfd55ef6490b4df12de5c46a5864929d88584ba6016d415375115953d15e6c7458a5e037f9282427
+  languageName: node
+  linkType: hard
+
+"@octokit/endpoint@npm:^10.1.3":
+  version: 10.1.3
+  resolution: "@octokit/endpoint@npm:10.1.3"
+  dependencies:
+    "@octokit/types": "npm:^13.6.2"
+    universal-user-agent: "npm:^7.0.2"
+  checksum: 10/22a03e106ed66005d48a26eebd9bcb95d418b150ac25eb456dcd00623b658175644d3c7e38474549004851f5bc7aecf2da623cd3227d9620f89e2a080174bfc0
+  languageName: node
+  linkType: hard
+
+"@octokit/graphql@npm:^8.1.2":
+  version: 8.2.1
+  resolution: "@octokit/graphql@npm:8.2.1"
+  dependencies:
+    "@octokit/request": "npm:^9.2.2"
+    "@octokit/types": "npm:^13.8.0"
+    universal-user-agent: "npm:^7.0.0"
+  checksum: 10/d247ef0c73ef8ffdb222e9724514ee4f64ff7195bd71da795db99e39d1e28d3b4c45b9c4a9fc151e263e01ecb259019f74f67a92d022b57fe5b876b720a4bb91
+  languageName: node
+  linkType: hard
+
+"@octokit/openapi-types@npm:^24.2.0":
+  version: 24.2.0
+  resolution: "@octokit/openapi-types@npm:24.2.0"
+  checksum: 10/000897ebc6e247c2591049d6081e95eb5636f73798dadd695ee6048496772b58065df88823e74a760201828545a7ac601dd3c1bcd2e00079a62a9ee9d389409c
+  languageName: node
+  linkType: hard
+
+"@octokit/plugin-paginate-rest@npm:^11.4.2":
+  version: 11.6.0
+  resolution: "@octokit/plugin-paginate-rest@npm:11.6.0"
+  dependencies:
+    "@octokit/types": "npm:^13.10.0"
+  peerDependencies:
+    "@octokit/core": ">=6"
+  checksum: 10/4b9e1ca479a0c577fe3dade6133b12ff172b6606586eb31ba2b7c7f651e0a173d365d45c5c7512e83659860a36fc4021ad6c0582be4698504fbfc201be17d164
+  languageName: node
+  linkType: hard
+
+"@octokit/plugin-request-log@npm:^5.3.1":
+  version: 5.3.1
+  resolution: "@octokit/plugin-request-log@npm:5.3.1"
+  peerDependencies:
+    "@octokit/core": ">=6"
+  checksum: 10/a27e163282c8d0ba8feee4d3cbbd1b62e1aa89a892877f7a9876fc17ddde3e1e1af922e6664221a0cabae99b8a7a2a5215b9ec2ee5222edb50e06298e99022b0
+  languageName: node
+  linkType: hard
+
+"@octokit/plugin-rest-endpoint-methods@npm:^13.3.0":
+  version: 13.5.0
+  resolution: "@octokit/plugin-rest-endpoint-methods@npm:13.5.0"
+  dependencies:
+    "@octokit/types": "npm:^13.10.0"
+  peerDependencies:
+    "@octokit/core": ">=6"
+  checksum: 10/bfd4da842b1a867a508baedc41788ffd84b0232814d9eff5551f460343685a52d91c3446133451cea50c79feee0ee64881431684ac86c3c62ab12212cd75c072
+  languageName: node
+  linkType: hard
+
+"@octokit/request-error@npm:^6.1.7":
+  version: 6.1.7
+  resolution: "@octokit/request-error@npm:6.1.7"
+  dependencies:
+    "@octokit/types": "npm:^13.6.2"
+  checksum: 10/02273f6388f1fa8e9962f5eeddffac784454200fa291d9e2333eeaa53f70fbf3fb8d9bca191f38457c455dda758b95c8db50167085cfd6f97dd7a67a5aff452d
+  languageName: node
+  linkType: hard
+
+"@octokit/request@npm:^9.2.1, @octokit/request@npm:^9.2.2":
+  version: 9.2.2
+  resolution: "@octokit/request@npm:9.2.2"
+  dependencies:
+    "@octokit/endpoint": "npm:^10.1.3"
+    "@octokit/request-error": "npm:^6.1.7"
+    "@octokit/types": "npm:^13.6.2"
+    fast-content-type-parse: "npm:^2.0.0"
+    universal-user-agent: "npm:^7.0.2"
+  checksum: 10/32d393de86c1a4cc58b605e74fefc0284837c01eca7c4cb1e56e5cf71b3f1b27c76acaae7d333fb43f5478a967c05e9861bc405ce85eaacd158942911adb7943
+  languageName: node
+  linkType: hard
+
+"@octokit/rest@npm:^21.1.1":
+  version: 21.1.1
+  resolution: "@octokit/rest@npm:21.1.1"
+  dependencies:
+    "@octokit/core": "npm:^6.1.4"
+    "@octokit/plugin-paginate-rest": "npm:^11.4.2"
+    "@octokit/plugin-request-log": "npm:^5.3.1"
+    "@octokit/plugin-rest-endpoint-methods": "npm:^13.3.0"
+  checksum: 10/34a0088c19a202e64bb32bfc939411b96267cf4b38773c4483957600f9d5669381bcfe86f3078d1e03cade9d289dc95e196422eac3c1d0939aaba25a78cce9a7
+  languageName: node
+  linkType: hard
+
+"@octokit/types@npm:^13.10.0, @octokit/types@npm:^13.6.2, @octokit/types@npm:^13.8.0":
+  version: 13.10.0
+  resolution: "@octokit/types@npm:13.10.0"
+  dependencies:
+    "@octokit/openapi-types": "npm:^24.2.0"
+  checksum: 10/32f8f5010d7faae128b0cdd0c221f0ca8c3781fe44483ecd87162b3da507db667f7369acda81340f6e2c9c374d9a938803409c6085c2c01d98210b6c58efb99a
+  languageName: node
+  linkType: hard
+
 "@opentelemetry/api-logs@npm:0.53.0":
   version: 0.53.0
   resolution: "@opentelemetry/api-logs@npm:0.53.0"
@@ -12137,6 +12261,7 @@ __metadata:
     "@currents/playwright": "npm:^1.11.3"
     "@electron/fuses": "npm:^1.8.0"
     "@electron/notarize": "npm:2.5.0"
+    "@octokit/rest": "npm:^21.1.1"
     "@playwright/browser-chromium": "npm:^1.51.0"
     "@playwright/browser-firefox": "npm:^1.51.0"
     "@playwright/browser-webkit": "npm:^1.51.0"
@@ -16319,6 +16444,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"before-after-hook@npm:^3.0.2":
+  version: 3.0.2
+  resolution: "before-after-hook@npm:3.0.2"
+  checksum: 10/57dfee78930276a740559552460a83f31c605e0164f02f170f71352aa1f4f5fb2c1632ac3bcba06ba711c32bd88b7e3c82431428e0c4984fbd2336faa78cf08c
+  languageName: node
+  linkType: hard
+
 "better-opn@npm:^3.0.2, better-opn@npm:~3.0.2":
   version: 3.0.2
   resolution: "better-opn@npm:3.0.2"
@@ -22985,6 +23117,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"fast-content-type-parse@npm:^2.0.0":
+  version: 2.0.1
+  resolution: "fast-content-type-parse@npm:2.0.1"
+  checksum: 10/ec361f5b79254259dadc8192005ceaa54789e63779f144ec3033bd54710f7cb666d47528aa0b12ea29da13f812a0c5f78ce7bc7d779f400bc612f2c65a5972d5
+  languageName: node
+  linkType: hard
+
 "fast-copy@npm:^3.0.2":
   version: 3.0.2
   resolution: "fast-copy@npm:3.0.2"
@@ -40743,6 +40882,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"universal-user-agent@npm:^7.0.0, universal-user-agent@npm:^7.0.2":
+  version: 7.0.2
+  resolution: "universal-user-agent@npm:7.0.2"
+  checksum: 10/3f02cb6de0bb9fbaf379566bd0320d8e46af6e4358a2e88fce7e70687ed7b48b37f479d728bb22f4204a518e363f3038ac4841c033af1ee2253f6428a6c67e53
+  languageName: node
+  linkType: hard
+
 "universalify@npm:^0.1.0":
   version: 0.1.2
   resolution: "universalify@npm:0.1.2"


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T08:36:12Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts (2)</summary><blockquote>

`242-243`: _⚠️ Potential issue_

**Fix incomplete string escaping for GraphQL query**

The current implementation only escapes double quotes but fails to handle other special characters like backslashes, newlines, and tabs which could break the GraphQL query or lead to injection vulnerabilities.



Implement more comprehensive escaping:

```diff
- title: "${title.replace(/"/g, '\\"')}"
- body: "${body.replace(/"/g, '\\"')}"
+ title: "${title.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')}"
+ body: "${body.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')}"
```

---

`174-197`: _🛠️ Refactor suggestion_

**Validate and properly format the valueOrOptionId parameter**

The `setItemValue` method doesn't validate or properly format the `valueOrOptionId` parameter, which could lead to GraphQL syntax errors.


Implement proper formatting and validation:

```diff
async setItemValue(
    projectId: string,
    itemId: string,
    fieldId: string,
    valueOrOptionId: ValueOrOptionId,
): Promise<void> {
+   let formattedValue: string;
+   
+   if (typeof valueOrOptionId === 'string') {
+       formattedValue = `"${valueOrOptionId.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
+   } else if (valueOrOptionId.optionId) {
+       formattedValue = `{ optionId: "${valueOrOptionId.optionId}" }`;
+   } else {
+       throw new Error('Invalid valueOrOptionId format');
+   }

    const mutation = `
      mutation {
        updateProjectV2ItemFieldValue(
        input: {
          projectId: "${projectId}"
          itemId: "${itemId}"
          fieldId: "${fieldId}"
-         value: ${valueOrOptionId}
+         value: ${formattedValue}
        }) {
          projectV2Item {
            id
          }
        }
      }
  `;

+   try {
        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+   } catch (error) {
+       this.logger.logError(`Failed to set item value for field ${fieldId}:`, error);
+       throw error;
+   }
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (12)</summary><blockquote>

<details>
<summary>docs/tests/e2e-playwright-github-reporter.md (3)</summary><blockquote>

`42-46`: **Consider using the correct “to-do” spelling in status values**

A small editorial nit: the noun form should be “To-do” (with a hyphen) to distinguish it from the verb “to do”. This avoids grammar-check warnings and keeps wording consistent.


```diff
- -   **Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc.)
+-   **Status** - Test outcome (To-do, In Progress, Done PASS, Done FAIL, etc.)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[grammar] ~42-~42: It appears that a hyphen is missing in the noun “To-do” (= task) or did you mean the verb “to do”?
Context: ...tadata:  -   **Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc...

(TO_DO_HYPHEN)

</details>

</details>

---

`83-85`: **Document how `PROJECT_NAME` is set**

The code currently hard-codes `PROJECT_NAME = 'Test Results 35'`. The docs imply it is configurable, but there is no guidance on how that value is derived or overridden (CI var? `.env`?). A short sentence (or an ENV variable example) would prevent confusion when running locally.

---

`115-116`: **Minor grammar – add articles for clarity**

“Add or change value” → “add or change **the** value”; “in corresponding object” → “in **the** corresponding object”.  
Not critical, but improves readability.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ...erKeys in getterByKey  To add or change value of annotation, simply change or add new...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

---

[uncategorized] ~115-~115: You might be missing the article “an” here.
Context: ... getterByKey  To add or change value of annotation, simply change or add new enum value in...

(AI_EN_LECTOR_MISSING_DETERMINER_AN)

---

[uncategorized] ~115-~115: You might be missing the article “a” here.
Context: ...lue of annotation, simply change or add new enum value in corresponding object in `...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

---

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ... simply change or add new enum value in corresponding object in `testAnnotations.ts` 

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (1)</summary><blockquote>

`122-152`: **Serial field updates slow down large test suites**

Each project-field update is awaited sequentially. With many fields/tests this can add minutes.

You can issue them in parallel while still using `scheduleAction` for each:

```diff
-for (const { name, value } of report.projectValues) {
-    const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(fields, name, value);
-    await scheduleAction(() => {
-        ...
-    }, RETRY_CONF);
-}
+await Promise.all(
+    report.projectValues.map(({ name, value }) => {
+        const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(fields, name, value);
+        return scheduleAction(() =>
+            this.graphQLClient.setItemValue(
+                this.projectId,
+                issueNodeId,
+                fieldId,
+                valueOrOptionId,
+            ), RETRY_CONF);
+    }),
+);
```

The reporter already tracks async operations, so concurrency is safe.  
 [performance]

</blockquote></details>
<details>
<summary>packages/suite-desktop-core/e2e/support/annotations.ts (2)</summary><blockquote>

`83-98`: **Status mapping treats manual tests as “Auto”**

`status` returns `AutoPass/AutoFail` even for tests tagged `@group=manual`.  
If the project board distinguishes manual vs automated, consider a separate enum (e.g., `ManualPass/ManualFail`) or suffix. Otherwise manual results may be mis-categorised in the “Status” field.

---

`40-46`: **Potential runtime cast issue in `createTestAnnotation`**

`needsFormatting` implies `value` is `string[]`, but the cast is unchecked. A defensive check avoids accidental runtime `join()` on a string.

```diff
-if (annotation.needsFormatting) {
-    formattedAnnotations.push({ type, description: formatList(value as string[]) });
+if (annotation.needsFormatting) {
+    if (!Array.isArray(value)) {
+        throw new Error(`Annotation '${key}' expects an array.`);
+    }
+    formattedAnnotations.push({ type, description: formatList(value) });
 } else {
```

</blockquote></details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts (3)</summary><blockquote>

`96-116`: **Consider implementing pagination handling for projects**

The method fetches only the first 30 projects matching the query. For organizations with many projects, this might not return all matching results.


Consider implementing a solution that handles pagination to retrieve all matching projects:

```diff
async getProjectFromOrganization(
    organization: string,
    projectName: string,
): Promise<Project[]> {
    this.logger.log(`Fetching projects for organization: ${organization}`);
+   const projects: Project[] = [];
+   let hasNextPage = true;
+   let endCursor = null;

+   while (hasNextPage) {
+       const afterParam = endCursor ? `, after: "${endCursor}"` : '';
        const query = `
            query {
              organization(login: "${organization}") {
-                projectsV2(first: 30, query: "${projectName}", orderBy: {field: NUMBER, direction: ASC}) {
+                projectsV2(first: 100, query: "${projectName}", orderBy: {field: NUMBER, direction: ASC}${afterParam}) {
+                  pageInfo {
+                    hasNextPage
+                    endCursor
+                  }
                  nodes {
                    id
                    title
                    number
                  }
                }
              }
            }
        `;

        const response = await this.octokit.graphql<ProjectQueryResponse>(query);
+       projects.push(...response.organization.projectsV2.nodes);
+       
+       hasNextPage = response.organization.projectsV2.pageInfo.hasNextPage;
+       endCursor = response.organization.projectsV2.pageInfo.endCursor;
    }

    this.logger.log('Successfully retrieved organization projects');

-   return response.organization.projectsV2.nodes;
+   return projects;
}
```

You'll also need to update the `ProjectQueryResponse` interface in `types.ts` to include the `pageInfo` field.

---

`139-172`: **Consider implementing pagination for project fields**

Similar to the project retrieval method, this method only fetches the first 20 fields. For projects with many custom fields, this might not return all fields.


Consider implementing pagination handling similar to the suggestion for the project retrieval method.

---

`231-257`: **Implement retry logic for network resilience**

The GitHub API might occasionally experience rate limiting or temporary issues. Adding retry logic would make the integration more robust.


Consider implementing a retry mechanism:

```diff
+ private async withRetry<T>(operation: () => Promise<T>, maxRetries = 3, delay = 1000): Promise<T> {
+     let lastError: Error | undefined;
+     
+     for (let attempt = 1; attempt <= maxRetries; attempt++) {
+         try {
+             return await operation();
+         } catch (error) {
+             lastError = error as Error;
+             
+             // Check if we should retry (e.g., rate limit or network error)
+             if (this.isRetryableError(error) && attempt < maxRetries) {
+                 this.logger.log(`Operation failed, retrying (${attempt}/${maxRetries})...`);
+                 await new Promise(resolve => setTimeout(resolve, delay * attempt));
+             } else {
+                 break;
+             }
+         }
+     }
+     
+     throw lastError;
+ }
+ 
+ private isRetryableError(error: any): boolean {
+     // Check for rate limit errors or network issues
+     return error.status === 429 || 
+            error.status >= 500 || 
+            error.message?.includes('network') ||
+            error.message?.includes('timeout');
+ }

async createDraftIssueInProject(
    projectId: string,
    title: string,
    body: string,
): Promise<string> {
-   const mutation = `...`;
-   const response = await this.octokit.graphql<AddDraftIssueResponse>(mutation);
-   const issueId = response.addProjectV2DraftIssue.projectItem.id;
-   return issueId;

+   return this.withRetry(async () => {
+       const mutation = `...`;
+       const response = await this.octokit.graphql<AddDraftIssueResponse>(mutation);
+       const issueId = response.addProjectV2DraftIssue.projectItem.id;
+       return issueId;
+   });
}
```

Apply this pattern to other methods in the class.

</blockquote></details>
<details>
<summary>packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts (1)</summary><blockquote>

`88-94`: **Consider adding full device names for better readability**

The `DeviceModel` enum uses abbreviations which might not be immediately clear to all team members.


Consider adding full device names to improve readability:

```diff
export enum DeviceModel {
-   T1B1 = 'T1B1',
-   T2T1 = 'T2T1',
-   T2B1 = 'T2B1',
-   T3T1 = 'T3T1',
-   Unknown = 'Unknown',
+   T1B1 = 'Trezor One (bootloader mode)',
+   T2T1 = 'Trezor T (normal mode)',
+   T2B1 = 'Trezor T (bootloader mode)',
+   T3T1 = 'Trezor Safe 3 (normal mode)',
+   Unknown = 'Unknown Device',
}
```

</blockquote></details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/types.ts (2)</summary><blockquote>

`1-5`: **Consider stronger typing for logging functions**

The `LoggingFunctions` interface uses `any` type for log arguments, which could lead to type safety issues.


While `any` is common for logging functions, consider adding generic type parameters or more specific types:

```diff
export interface LoggingFunctions {
-   log: (...args: any[]) => void;
-   logError: (...args: any[]) => void;
+   log: (message: string, ...args: unknown[]) => void;
+   logError: (message: string, error?: Error | unknown, ...args: unknown[]) => void;
    logResponse: (label: string, response: any) => void;
}
```

---

`50-56`: **Add pagination information to response interfaces**

To support implementing pagination for fields and projects, the response interfaces should include pagination information.


```diff
export interface ProjectFieldsResponse {
    node: {
        fields: {
            nodes: ProjectField[];
+           pageInfo?: {
+               hasNextPage: boolean;
+               endCursor: string | null;
+           };
        };
    };
}

export interface ProjectQueryResponse {
    organization: {
        projectsV2: {
            nodes: Project[];
+           pageInfo: {
+               hasNextPage: boolean;
+               endCursor: string | null;
+           };
        };
    };
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8b952550ef51d8971daa6d01457cf004942b7a5f and 341bebbaecdd97a9614d2193e530d927597e5a8c.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `yarn.lock` is excluded by `!**/yarn.lock`, `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `docs/tests/e2e-playwright-github-reporter.md` (1 hunks)
* `package.json` (1 hunks)
* `packages/suite-desktop-core/.example.env` (1 hunks)
* `packages/suite-desktop-core/e2e/support/annotations.ts` (2 hunks)
* `packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts` (3 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/types.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts` (2 hunks)
* `packages/suite-desktop-core/package.json` (2 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts (3)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (2)</summary>

* `octokit` (46-54)
* `projectId` (64-70)

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/types.ts (11)</summary>

* `LoggingFunctions` (1-5)
* `CreateProjectMutation` (14-20)
* `CreateFieldResponse` (66-72)
* `Project` (7-11)
* `ProjectQueryResponse` (22-28)
* `AddIssueToProjectResponse` (30-36)
* `ProjectField` (43-48)
* `ProjectFieldsResponse` (50-56)
* `ValueOrOptionId` (86-86)
* `UpdateProjectItemFieldResponse` (58-64)
* `AddDraftIssueResponse` (74-84)

</details>
<details>
<summary>suite-common/trading/src/invityAPI.ts (1)</summary>

* `options` (129-153)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/tests/e2e-playwright-github-reporter.md</summary>

[grammar] ~42-~42: It appears that a hyphen is missing in the noun “To-do” (= task) or did you mean the verb “to do”?
Context: ...tadata:  -   **Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc...

(TO_DO_HYPHEN)

---

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ...erKeys in getterByKey  To add or change value of annotation, simply change or add new...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

---

[uncategorized] ~115-~115: You might be missing the article “an” here.
Context: ... getterByKey  To add or change value of annotation, simply change or add new enum value in...

(AI_EN_LECTOR_MISSING_DETERMINER_AN)

---

[uncategorized] ~115-~115: You might be missing the article “a” here.
Context: ...lue of annotation, simply change or add new enum value in corresponding object in `...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

---

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ... simply change or add new enum value in corresponding object in `testAnnotations.ts` 

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: Unit Tests
* GitHub Check: Releases revision Checks
* GitHub Check: Other Checks
* GitHub Check: Linting and formatting
* GitHub Check: Type Checking
* GitHub Check: Build libs for publishing
* GitHub Check: PR-check   / node-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: PR-check   / web-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: build-app
* GitHub Check: build-deploy
* GitHub Check: test
* GitHub Check: Analyze with CodeQL (javascript)

</details>

<details>
<summary>🔇 Additional comments (18)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/.example.env (1)</summary>

`8-9`: **LGTM! Clear documentation for the new GitHub token.**

The addition of the commented-out GitHub token variable with descriptive documentation is helpful for developers who will use the new GitHub Reporter functionality.

</details>
<details>
<summary>package.json (2)</summary>

`119-119`: **Added Octokit REST dependency correctly.**

Adding the Octokit REST client makes sense for the GitHub Reporter feature that needs to interact with GitHub's API.

---

`18-19`: **New npm scripts look good.**

These scripts properly implement the GitHub Reporter requirements mentioned in the PR objectives, allowing for both web and desktop test reporting via the custom reporter.

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts (2)</summary>

`9-9`: **LGTM on the import.**

The import of TestAnnotationType is needed for the device model annotation that follows.

---

`102-106`: **Good integration of device model metadata.**

This annotation properly captures the emulator model information that will be used by the GitHub reporter to provide more detailed test context. The comment is helpful for explaining the purpose of this addition.

</details>
<details>
<summary>packages/suite-desktop-core/package.json (2)</summary>

`18-19`: **Well-structured npm scripts for GitHub reporting.**

These new scripts align with the GitHub Reporter implementation described in the PR objectives, making it easy to run tests with the custom reporter.

---

`58-58`: **Correctly added package dependency.**

Adding the Octokit REST client as a devDependency is appropriate since it's used for the GitHub Reporter in test scenarios.

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (2)</summary>

`46-55`: _⚠️ Potential issue_

**Fail fast when `GITHUB_TOKEN` is missing**

`Octokit` is instantiated with possibly `undefined` auth. Add an explicit guard so users get an actionable error instead of a 401 later.

```diff
 if (!process.env.GITHUB_TOKEN) {
-    this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+    throw new Error('GITHUB_TOKEN is not set – GitHub reporter cannot run.');
 }
+this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
```




> Likely an incorrect or invalid review comment.

---

`324-349`: <details>
<summary>❌ Incorrect review comment</summary>

**GraphQL payload building looks off – braces are part of the string**

`valueOrOptionId` includes curly braces.  If `setItemValue` expects variables (IDs / text) rather than a literal snippet of GraphQL, the braces will break the mutation. Consider passing a typed argument instead and let the GraphQL client format it.

```diff
-return { fieldId: field.id, valueOrOptionId: `{ singleSelectOptionId: "${option.id}" }` };
+return { fieldId: field.id, valueOrOptionId: option.id };
...
-return { fieldId: field.id, valueOrOptionId: `{ text: "${value}" }` };
+return { fieldId: field.id, valueOrOptionId: value };
```

Please double-check `GitHubGraphQLClient.setItemValue` signature to avoid runtime errors.

---

🏁 Script executed:

```shell
#!/bin/bash
# Locate the GraphQL client class and its setItemValue method
rg "class GitHubGraphQLClient" -n
rg "setItemValue" -n
```

Length of output: 408

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the setItemValue method signature and surrounding context
sed -n '160,220p' packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
# Display the usage site in the reporter for context
sed -n '130,180p' packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
```

Length of output: 4079

---

🏁 Script executed:

```shell
#!/bin/bash
# Find the definition of ValueOrOptionId in the GitHubGraphQLClient file
rg "type ValueOrOptionId" -n packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts || rg "interface ValueOrOptionId" -n packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
```

Length of output: 212

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for ValueOrOptionId definition across the repository
rg "ValueOrOptionId" -n .
```

Length of output: 370

---



</details>

**No change needed – braces are required for valid GraphQL literals**  
The `setItemValue` mutation injects `value: ${valueOrOptionId}` directly into the GraphQL string, so callers must supply the full object literal (e.g. `{ singleSelectOptionId: "…" }` or `{ text: "…" }`). Stripping the braces and passing only the raw ID or text would produce invalid GraphQL syntax. 

> Likely an incorrect or invalid review comment.

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts (1)</summary>

`17-21`: **Well-structured client for GitHub GraphQL API**

The class is well-structured with a clean constructor accepting the necessary dependencies. Using the Octokit client and logging functions as injected dependencies follows good dependency injection practices.

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts (6)</summary>

`1-11`: **Well-structured test annotation type enums**

The `TestAnnotationType` enum provides a clear organization of the different annotation types, with appropriate renaming of `TestCase` to camelCase and addition of the new `DeviceModel` annotation.

---

`13-42`: **Good addition of NotCategorized to TestCategory**

Adding `NotCategorized` to the `TestCategory` enum is a good improvement for handling tests that don't fit into predefined categories. This will help ensure all tests have proper categorization.

---

`52-58`: **Good color mapping for test priorities**

The color mapping for test priorities follows a logical progression from red (critical) to green (low), making it visually intuitive in the GitHub UI.

---

`65-86`: **Comprehensive test status enum with appropriate color coding**

The `TestStatus` enum and associated colors provide a clear way to track test status throughout the lifecycle. The color coding (red for failures, green for passes, yellow for in-progress) follows standard conventions.

---

`97-105`: **Well-designed BaseAnnotation interface**

The `BaseAnnotation` interface is well-designed with appropriate fields for name, key, value type, and options. The optional fields like `needsFormatting` provide flexibility for specific annotation needs.

---

`184-205`: **Well-organized annotation groupings**

The grouping of annotations into different arrays based on their usage context (body description, project fields, added to test) provides clear organization and makes it easy to understand how each annotation is used.

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/types.ts (2)</summary>

`7-48`: **Well-structured interface definitions for entities and responses**

The interfaces for `Project`, `ProjectField`, `FieldOption`, and related response types are well-structured and provide clear type definitions for the GraphQL API responses.

---

`86-86`: **Good use of union type for ValueOrOptionId**

The `ValueOrOptionId` union type provides a clean way to handle both string values and option IDs for field values.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-23T21:56:55Z
user: login: HajekOndrej
body: I have a lot of questions and suggestions. Also Coderabbit's ideas seem mostly valid, so please check them.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T13:40:21Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T13:40:46Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:14:17Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:14:28Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:16:09Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T19:16:38Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:34:19Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T19:34:25Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:35:39Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:39:30Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:44:31Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:47:05Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:47:51Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:48:30Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:52:29Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T19:53:46Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T20:58:48Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T20:59:10Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T20:59:47Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-24T21:02:23Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 9**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts (1)</summary><blockquote>

`12-15`: **Hard-coded `PROJECT_NAME` – acknowledged as intentional in previous review**

Leaving this note only for traceability; no action requested in this PR phase.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (1)</summary><blockquote>

`225-241`: **Minor: quote-escaping duplicated – extract helper & cover edge-cases**

`{ text: "${value}" }` shares the same escaping weakness noted earlier (newlines, back-slashes).
A small helper like `escapeGraphQLString(value)` would keep this logic DRY and safer.

</blockquote></details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts (4)</summary><blockquote>

`21-21`: **Consider adding pagination support for fields**

The query only fetches the first 20 fields, which might be insufficient for complex projects. GitHub's GraphQL API supports cursor-based pagination.


```diff
-              fields(first: 20) {
+              fields(first: 100) {
                nodes {
```

For projects with many fields, consider implementing proper pagination with cursors:

```typescript
fields(first: 100, after: $cursor) {
  nodes {
    // field properties
  }
  pageInfo {
    hasNextPage
    endCursor
  }
}
```

---

`125-149`: **Implement pagination for `getProjectFromOrganization`**

The current implementation only fetches the first 30 projects, which might not be sufficient for large organizations.


```diff
async getProjectFromOrganization(
    organization: string,
    projectName: string,
+    limit: number = 30
): Promise<Project[]> {
    this.logger.log(`Fetching projects for organization: ${organization}`);
+    
+    // Sanitize inputs
+    const sanitizedProjectName = projectName.replace(/"/g, '\\"');
+    const sanitizedOrg = organization.replace(/"/g, '\\"');

    const query = `
        query {
-          organization(login: "${organization}") {
-            projectsV2(first: 30, query: "${projectName}", orderBy: {field: NUMBER, direction: ASC}) {
+          organization(login: "${sanitizedOrg}") {
+            projectsV2(first: ${limit}, query: "${sanitizedProjectName}", orderBy: {field: NUMBER, direction: ASC}) {
              nodes {
                id
                title
                number
              }
+             pageInfo {
+               hasNextPage
+               endCursor
+             }
            }
          }
        }
    `;

-    const response = await this.octokit.graphql<ProjectQueryResponse>(query);
-    this.logger.log('Successfully retrieved organization projects');
-
-    return response.organization.projectsV2.nodes;
+    try {
+        const response = await this.octokit.graphql<ProjectQueryResponse>(query);
+        this.logger.log('Successfully retrieved organization projects');
+        return response.organization.projectsV2.nodes;
+    } catch (error) {
+        this.logger.error(`Failed to fetch projects: ${error.message}`);
+        throw new Error(`Failed to fetch projects for ${organization}: ${error.message}`);
+    }
}
```

For complete pagination support, you would need to implement cursor-based fetching with multiple requests when hasNextPage is true.

---

`49-54`: **Consider adding retry logic for API calls**

GitHub API calls can fail due to rate limits or network issues. Adding a retry mechanism would improve reliability.


```diff
export class ProjectRequests {
    constructor(
        private readonly octokit: Octokit,
        private readonly logger: LoggingFunctions,
+        private readonly maxRetries: number = 3,
+        private readonly retryDelay: number = 1000,
    ) {}
    
+    private async retryOperation<T>(operation: () => Promise<T>, operationName: string): Promise<T> {
+        let lastError: Error | null = null;
+        
+        for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
+            try {
+                return await operation();
+            } catch (error) {
+                lastError = error;
+                
+                // Don't retry if it's not a retryable error (e.g., 4xx except 429)
+                if (error.status && error.status !== 429 && error.status < 500) {
+                    throw error;
+                }
+                
+                this.logger.log(`Attempt ${attempt} failed for ${operationName}: ${error.message}`);
+                
+                if (attempt < this.maxRetries) {
+                    const delay = this.retryDelay * Math.pow(2, attempt - 1);
+                    this.logger.log(`Retrying in ${delay}ms...`);
+                    await new Promise(resolve => setTimeout(resolve, delay));
+                }
+            }
+        }
+        
+        throw lastError || new Error(`All ${this.maxRetries} attempts failed for ${operationName}`);
+    }
}
```

Then update each method to use this retry logic:

```typescript
async createProject(...) {
    return this.retryOperation(
        async () => {
            // Existing implementation
        },
        'createProject'
    );
}
```

---

`1-11`: **Consider adding rate limit handling**

GitHub's API has rate limits that could affect the reporter's functionality during heavy usage. Consider adding rate limit detection and backoff logic.


You could extend the Octokit instance configuration to include automatic retry for rate limits:

```typescript
import { Octokit } from '@octokit/rest';
import { retry } from '@octokit/plugin-retry';

// Create a custom Octokit class with retry plugin
const RetryOctokit = Octokit.plugin(retry);

// Then use it with:
const octokit = new RetryOctokit({
  auth: process.env.GITHUB_TOKEN,
  retry: {
    doNotRetry: ['429', '404'],
    retries: 3,
    retryAfter: 10,
  },
});
```

Alternatively, add rate limit detection in your error handling:

```typescript
try {
  // API call
} catch (error) {
  if (error.status === 403 && error.headers && error.headers['x-ratelimit-remaining'] === '0') {
    const resetTimestamp = parseInt(error.headers['x-ratelimit-reset'], 10) * 1000;
    const waitTime = resetTimestamp - Date.now();
    console.log(`Rate limit exceeded. Waiting ${waitTime/1000} seconds before retrying.`);
    await new Promise(resolve => setTimeout(resolve, waitTime));
    // Retry the call
  } else {
    throw error;
  }
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 341bebbaecdd97a9614d2193e530d927597e5a8c and abd928dd00f6bad13d36208dde0e323811910d04.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `packages/suite-desktop-core/e2e/support/annotations.ts` (2 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* packages/suite-desktop-core/e2e/support/annotations.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (2)</summary>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts (2)</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (2)</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (13)</summary>

* GitHub Check: PR-check   / node-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: PR-check   / node-override init-api-flaky
* GitHub Check: PR-check   / web-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: PR-check   / web-override init-api-flaky
* GitHub Check: build-app
* GitHub Check: build-deploy
* GitHub Check: build-deploy
* GitHub Check: transport-e2e-test
* GitHub Check: Analyze with CodeQL (javascript)
* GitHub Check: Setup and Cache Dependencies
* GitHub Check: test
* GitHub Check: connect-web-e2e
* GitHub Check: Socket Security: Pull Request Alerts

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:25:57Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:29:40Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:30:30Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:31:00Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:31:35Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:32:16Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:33:47Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:34:11Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:38:55Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:39:21Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts (2)</summary><blockquote>

`1-1`: **Import Octokit as a type-only dependency**  

`IssueRequests` only receives an already-constructed `Octokit` instance; it never calls `new Octokit()` or accesses static members.  Keeping the runtime import here re-introduces the same ESM-resolution problem we discussed in the previous commit and bloats the bundle needlessly.

```diff
-import { Octokit } from '@octokit/rest';
+import type { Octokit } from '@octokit/rest';
```

---

`73-88`: **Draft-issue mutation repeats the unsafe string interpolation pattern**  

The same injection/escaping concerns apply here. Re-use the variable-based approach demonstrated above; it removes the need for `escapeGraphQLString` and makes the payload far more readable.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts (1)</summary><blockquote>

`64-66`: **`escapeGraphQLString` does not cover new-lines or other control characters**  

You escape `\` and `"`, but a plain line-break inside `title` or `body` still breaks the query string. Rather than chasing every edge-case, prefer GraphQL variables and drop this helper altogether.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between abd928dd00f6bad13d36208dde0e323811910d04 and 766f178a77b72bb96e3aa10baf1eeef8817f02a1.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts (2)</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts:1-1
Timestamp: 2025-04-25T06:39:20.248Z
Learning: The `@octokit/rest` package should be imported as a type-only dependency in files that need just the Octokit type, using `import type { Octokit } from '@octokit/rest';` to prevent runtime ESM resolution errors. This is especially important since the actual implementation is dynamically imported at runtime in GitHubReporter.ts.
```

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

</details>

</details><details>
<summary>🧬 Code Graph Analysis (1)</summary>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts (2)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/types.ts (5)</summary>

* `AddIssueToProjectResponse` (30-36)
* `ProjectField` (43-48)
* `ValueOrOptionId` (86-86)
* `UpdateProjectItemFieldResponse` (58-64)
* `AddDraftIssueResponse` (74-84)

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts (2)</summary>

* `getProjectFields` (13-47)
* `getProjectFields` (151-157)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: PR-check   / node-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: PR-check   / web-override init-api-flaky
* GitHub Check: PR-check   / node-override init-api-flaky
* GitHub Check: PR-check   / web-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: build-app
* GitHub Check: build-deploy
* GitHub Check: Setup and Cache Dependencies
* GitHub Check: build-deploy
* GitHub Check: Analyze with CodeQL (javascript)
* GitHub Check: test
* GitHub Check: connect-web-e2e
* GitHub Check: transport-e2e-test

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:40:33Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:41:49Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:42:09Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:43:40Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:44:09Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (2)</summary><blockquote>

`11-15`: **Retry configuration looks solid**

The retry configuration with 3 attempts and 500ms gaps is reasonable for handling transient GitHub API issues. I see that the hardcoded `VERBOSE` flag is intentional for this PR and will be made configurable in a follow-up.

---

`108-109`: **Race condition fix looks good**

The code now correctly sets `InitializationState.COMPLETED` after `gitHubProject.init()` completes, addressing the potential race condition mentioned in previous reviews.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (2)</summary><blockquote>

`197-210`: **Consider cache invalidation mechanism**

The project fields caching is good for performance, but there's no mechanism to refresh the cache if fields change during the test run.


```diff
 private async getProjectFields(): Promise<ProjectField[]> {
     if (this.cachedFields) {
         this.log('Using cached project fields');
 
         return this.cachedFields;
     }
 
     this.log(`Fetching fields for project ${this.gitHubProject.id}...`);
     const fields = await this.issueRequests.getProjectFields(this.gitHubProject.id);
     this.log(`Successfully retrieved fields for project ${this.gitHubProject.id}`);
     this.cachedFields = fields;
 
     return fields;
 }
+
+// Add method to allow cache invalidation if needed
+private invalidateFieldsCache(): void {
+    this.log('Invalidating project fields cache');
+    this.cachedFields = null;
+}
```

---

`252-270`: **Simplify state waiting logic**

The "wait until state changes" polling approach could be simplified using promises more directly.


```diff
 if (this.initState === InitializationState.NOT_STARTED) {
-    // Wait until state changes from NOT_STARTED to something else
-    await new Promise<void>((resolve, reject) => {
-        const checkInterval = setInterval(() => {
-            if (this.initState !== InitializationState.NOT_STARTED) {
-                clearInterval(checkInterval);
-                resolve();
-            }
-        }, 100);
-
-        setTimeout(() => {
-            clearInterval(checkInterval);
-            reject(new Error('Timed out waiting for onBegin initialization to start'));
-        }, 30_000);
-    });
-
-    // Now state should be changed, call ensureInitialized again to handle the new state
-    return this.waitForOnBeginInit();
+    // Wait for initialization to start
+    this.log('Waiting for initialization to start...');
+    let startTime = Date.now();
+    
+    while (this.initState === InitializationState.NOT_STARTED) {
+        if (Date.now() - startTime > 30_000) {
+            throw new Error('Timed out waiting for onBegin initialization to start');
+        }
+        await new Promise(resolve => setTimeout(resolve, 100));
+    }
+    
+    this.log(`Initialization state changed to ${this.initState}`);
+    return this.waitForOnBeginInit();
 }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 766f178a77b72bb96e3aa10baf1eeef8817f02a1 and 2ea5015de17352b1af740e19f52dd95d8362593a.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (3)</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts:1-1
Timestamp: 2025-04-25T06:39:20.248Z
Learning: The `@octokit/rest` package should be imported as a type-only dependency in files that need just the Octokit type, using `import type { Octokit } from '@octokit/rest';` to prevent runtime ESM resolution errors. This is especially important since the actual implementation is dynamically imported at runtime in GitHubReporter.ts.
```

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (8)</summary>

* GitHub Check: build-deploy
* GitHub Check: build-app
* GitHub Check: Setup and Cache Dependencies
* GitHub Check: test
* GitHub Check: Analyze with CodeQL (javascript)
* GitHub Check: build-deploy
* GitHub Check: transport-e2e-test
* GitHub Check: connect-web-e2e

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (6)</summary>

`1-10`: **LGTM: Proper type imports and dependencies**

The use of `import type { Octokit }` correctly addresses the conditional exports issue with the Octokit package, separating types from runtime imports. All required dependencies are properly imported.

---

`17-22`: **Well-defined initialization state enum**

The initialization state machine is well-defined with clear state transitions, making the reporter's lifecycle easier to track and debug.

---

`24-33`: **Class property naming follows best practices**

The underscore prefix for private fields with getters is a good pattern, especially given your use case where these properties need null checks and error handling before access.

---

`77-89`: **Good operation tracking pattern**

The `trackOperation` method effectively manages asynchronous operations, ensuring Playwright waits for them to complete before ending the test run.

---

`116-136`: **Effective test recording with retry handling**

The implementation of `onTestEnd` with proper waiting for initialization, test metadata extraction, and retry-protected issue creation is robust.

---

`243-276`: **Robust initialization waiting mechanism**

The `waitForOnBeginInit` method effectively handles all possible initialization states with proper timeout protection. The 30-second timeout should be sufficient for most environments.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:44:37Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:44:55Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:45:22Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:45:30Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:45:52Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:47:20Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:47:42Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:47:58Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:49:14Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T06:49:57Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T06:50:27Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T10:22:40Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T10:23:36Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T10:24:26Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (8)</summary><blockquote>

<details>
<summary>docs/tests/e2e-playwright-github-reporter.md (3)</summary><blockquote>

`42-42`: **Grammar correction for Status field description**

According to the static analysis hint, it appears that a hyphen is missing in "Todo". The correct form would be "To-do" (with a hyphen) when used as a noun representing a task.

```diff
-   **Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc.)
+   **Status** - Test outcome (To-do, In Progress, Done PASS, Done FAIL, etc.)
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[grammar] ~42-~42: It appears that a hyphen is missing in the noun “To-do” (= task) or did you mean the verb “to do”?
Context: ...tadata:  -   **Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc...

(TO_DO_HYPHEN)

</details>

</details>

---

`97-98`: **Add more details about the retry mechanism**

The current description mentions retry but doesn't explicitly explain the configuration values referenced in the code (3 attempts with 500ms delay).

```diff
-The GitHub Test Reporter implements a retry mechanism to handle network issues or temporary API failures. It uses the `scheduleAction` utility. Each API operation will be attempted up to 3 times with a 500ms delay between attempts.
+The GitHub Test Reporter implements a retry mechanism to handle network issues or temporary API failures. It uses the `scheduleAction` utility from `@trezor/utils`. Each API operation will be attempted up to 3 times with a 500ms delay between attempts as defined in the `RETRY_CONF` configuration.
```

---

`115-115`: **Grammar correction in extending instructions**

The sentence is missing several articles which would improve readability.

```diff
-To add or change value of annotation, simply change or add new enum value in corresponding object in `testAnnotations.ts`
+To add or change the value of an annotation, simply change or add a new enum value in the corresponding object in `testAnnotations.ts`
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ...erKeys in getterByKey  To add or change value of annotation, simply change or add new...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

---

[uncategorized] ~115-~115: You might be missing the article “an” here.
Context: ... getterByKey  To add or change value of annotation, simply change or add new enum value in...

(AI_EN_LECTOR_MISSING_DETERMINER_AN)

---

[uncategorized] ~115-~115: You might be missing the article “a” here.
Context: ...lue of annotation, simply change or add new enum value in corresponding object in `...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

---

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ... simply change or add new enum value in corresponding object in `testAnnotations.ts` 

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</blockquote></details>
<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (1)</summary><blockquote>

`138-157`: **Consider batching field updates for performance**

Currently, field updates are processed sequentially within a loop, which could lead to slower performance when dealing with many fields. Consider batching these updates to reduce the number of API calls.

```diff
 for (const { name, value } of report.projectValues) {
     const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(
         fields,
         name,
         value,
     );
     await scheduleAction(() => {
         this.log(`[${issueNodeId}] Updating field ${name}:"${value}"...`);

         return this.issueRequests.setItemValue(
             this.gitHubProject.id,
             issueNodeId,
             fieldId,
             valueOrOptionId,
         );
     }, RETRY_CONF);
     this.log(`[${issueNodeId}] Successfully updated field ${name}:"${value}"`);
 }
```

However, this optimization would require changes to the `issueRequests` interface to support batch updates, so it might be best considered for a future enhancement.

</blockquote></details>
<details>
<summary>packages/suite-desktop-core/e2e/support/annotations.ts (3)</summary><blockquote>

`83-98`: **Simplify status logic with early returns**

The current status logic could be simplified for better readability using early returns:

```diff
 get status(): string {
     // This condition covers manual and automated tests that are skipped
     if (this.test.outcome() === 'skipped') {
         return TestStatus.Todo;
     }

-    if (this.test.ok()) {
-        return TestStatus.AutoPass;
-    }
-
-    if (!this.test.ok()) {
-        return TestStatus.AutoFail;
-    }
-
-    return TestStatus.Todo;
+    return this.test.ok() ? TestStatus.AutoPass : TestStatus.AutoFail;
 }
```

This simplifies the logic and eliminates the unreachable `return TestStatus.Todo` statement at the end.

---

`135-137`: **Empty comment getter**

The `comment` getter currently returns an empty string. Is this intentional? If it's a placeholder for future implementation, consider adding a TODO comment to indicate that.

```diff
 get comment(): string {
-    return '';
+    // TODO: Implement comment extraction logic or keep empty as default
+    return '';
 }
```

---

`143-145`: **Consider making tag detection more robust**

The current implementation of `isManual` relies on a specific tag format. Consider making this more flexible to accommodate potential changes in tag format:

```diff
 get isManual(): boolean {
-    return this.test.tags.some(tag => tag.startsWith('@group=manual'));
+    return this.test.tags.some(tag => 
+        tag.startsWith('@group=manual') || 
+        tag === 'manual' || 
+        tag.includes('manual')
+    );
 }
```

Alternatively, you could extract this to a constant or configuration value to make it more maintainable if the tag format changes in the future.

</blockquote></details>
<details>
<summary>packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts (1)</summary><blockquote>

`52-58`: **Consider using a color enum instead of string literals**

Currently, colors are defined as string literals ("RED", "ORANGE", etc.). Consider creating a dedicated enum for colors to ensure consistency and type safety throughout the codebase:

```diff
+export enum ColorType {
+    RED = 'RED',
+    ORANGE = 'ORANGE',
+    YELLOW = 'YELLOW',
+    GREEN = 'GREEN',
+    BLUE = 'BLUE',
+    GRAY = 'GRAY',
+}

-export const TestPriorityColors: Record<TestPriority, string> = {
+export const TestPriorityColors: Record<TestPriority, ColorType> = {
-    [TestPriority.Critical]: 'RED',
+    [TestPriority.Critical]: ColorType.RED,
-    [TestPriority.High]: 'ORANGE',
+    [TestPriority.High]: ColorType.ORANGE,
-    [TestPriority.Medium]: 'YELLOW',
+    [TestPriority.Medium]: ColorType.YELLOW,
-    [TestPriority.Low]: 'GREEN',
+    [TestPriority.Low]: ColorType.GREEN,
-    [TestPriority.AsNecessary]: 'GRAY',
+    [TestPriority.AsNecessary]: ColorType.GRAY,
};
```

The same approach could be applied to `TestStatusColors`. This would provide better type checking and autocompletion support.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 8c929ad5417e691b81fd8eb3dc44064e5641dacb and aad1b336757c0c219c11397ad01940bdc6dc981c.

</details>

<details>
<summary>📒 Files selected for processing (11)</summary>

* `docs/tests/e2e-playwright-github-reporter.md` (1 hunks)
* `packages/suite-desktop-core/.example.env` (1 hunks)
* `packages/suite-desktop-core/e2e/support/annotations.ts` (2 hunks)
* `packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts` (3 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/types.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts` (2 hunks)
* `packages/suite-desktop-core/package.json` (2 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (4)</summary>

* packages/suite-desktop-core/.example.env
* packages/suite-desktop-core/package.json
* packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
* packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (3)</summary>

* packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts
* packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
* packages/suite-desktop-core/e2e/support/reporters/types.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (3)</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts:1-1
Timestamp: 2025-04-25T06:39:20.248Z
Learning: The `@octokit/rest` package should be imported as a type-only dependency in files that need just the Octokit type, using `import type { Octokit } from '@octokit/rest';` to prevent runtime ESM resolution errors. This is especially important since the actual implementation is dynamically imported at runtime in GitHubReporter.ts.
```

</details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/tests/e2e-playwright-github-reporter.md</summary>

[grammar] ~42-~42: It appears that a hyphen is missing in the noun “To-do” (= task) or did you mean the verb “to do”?
Context: ...tadata:  -   **Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc...

(TO_DO_HYPHEN)

---

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ...erKeys in getterByKey  To add or change value of annotation, simply change or add new...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

---

[uncategorized] ~115-~115: You might be missing the article “an” here.
Context: ... getterByKey  To add or change value of annotation, simply change or add new enum value in...

(AI_EN_LECTOR_MISSING_DETERMINER_AN)

---

[uncategorized] ~115-~115: You might be missing the article “a” here.
Context: ...lue of annotation, simply change or add new enum value in corresponding object in `...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

---

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ... simply change or add new enum value in corresponding object in `testAnnotations.ts` 

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (12)</summary>

* GitHub Check: PR-check   / node-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: PR-check   / web-override init-api-flaky
* GitHub Check: PR-check   / node-override init-api-flaky
* GitHub Check: PR-check   / web-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: build-deploy
* GitHub Check: build-deploy
* GitHub Check: transport-e2e-test
* GitHub Check: Setup and Cache Dependencies
* GitHub Check: test
* GitHub Check: connect-web-e2e
* GitHub Check: Analyze with CodeQL (javascript)
* GitHub Check: Socket Security: Pull Request Alerts

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts (2)</summary>

`11-15`: **Verify this implementation matches your intention for verbosity control**

I see that `VERBOSE` is hardcoded to `true`, which will cause all detailed logs to be printed. Based on your previous feedback, I understand this is intentional and will be made configurable through environment variables in a follow-up PR.

Just confirming this is still your plan for handling verbosity control.

---

`243-270`: **Good initialization handling pattern**

The implementation of `waitForOnBeginInit()` is robust and handles all possible initialization states correctly. The state machine approach with the polling mechanism ensures that tests will wait appropriately for initialization to complete or fail fast if initialization failed. This is a good pattern for managing asynchronous initialization that might be happening in parallel.

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/annotations.ts (1)</summary>

`174-195`: **Great implementation of type-safe getters**

The implementation of the getter mapping with a typed record is excellent. This approach makes the code more maintainable and type-safe compared to using string indexing with `this[key]`. The error message is also clear and helpful for debugging.

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts (1)</summary>

`184-195`: **Well-organized annotation groupings**

The explicit grouping of annotations into purpose-specific arrays (`annotationsForBodyDescription`, `annotationsForProjectFields`, and `annotationsAddedToTest`) is an excellent design pattern. This promotes a clear separation of concerns and makes it easy to understand how annotations are used in different contexts.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T10:36:20Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts (3)</summary><blockquote>

`187-195`: **Consider including categoryAnnotation in project fields**

The categoryAnnotation is defined but not included in annotationsForProjectFields. Test categories could be valuable for filtering and organizing tests in GitHub Projects. Consider adding it unless there's a specific reason to exclude it.

```diff
export const annotationsForProjectFields = [
    statusAnnotation,
    streamAnnotation,
    testRunAnnotation,
    priorityAnnotation,
    deviceModelAnnotation,
+   categoryAnnotation,
    commentAnnotation,
];
```

---

`1-206`: **Consider adding validation logic for annotation values**

The annotation system is well-structured but lacks validation logic to ensure values match their defined enums. Consider adding helper functions to validate annotation values against their expected types and options.

```typescript
// Example validation helper
export function validateAnnotationValue(
  annotation: BaseAnnotation,
  value: unknown
): boolean {
  if (annotation.valueType === 'SINGLE_SELECT' && annotation.valueOptions) {
    return typeof value === 'string' && annotation.valueOptions.includes(value);
  }
  if (annotation.valueType === 'TEXT') {
    return typeof value === 'string';
  }
  return false;
}
```

---

`197-205`: **Consider mechanism for default values**

The system doesn't specify how to handle missing annotations. Consider adding default values for important annotations to ensure consistent behavior when tests aren't fully annotated.

```diff
+// Default values for annotations when not explicitly provided
+export const defaultAnnotationValues = {
+  [categoryAnnotation.key]: TestCategory.NotCategorized,
+  [priorityAnnotation.key]: TestPriority.Medium,
+  [streamAnnotation.key]: TestStream.NotDefined,
+  [statusAnnotation.key]: TestStatus.Todo,
+  [deviceModelAnnotation.key]: DeviceModel.Unknown,
+};
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between aad1b336757c0c219c11397ad01940bdc6dc981c and f81a1a2cf00aa593784ccba1d723a6e62b13c3c1.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `yarn.lock` is excluded by `!**/yarn.lock`, `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (12)</summary>

* `docs/tests/e2e-playwright-github-reporter.md` (1 hunks)
* `packages/suite-desktop-core/.example.env` (1 hunks)
* `packages/suite-desktop-core/e2e/support/annotations.ts` (2 hunks)
* `packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts` (3 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/reporters/types.ts` (1 hunks)
* `packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts` (2 hunks)
* `packages/suite-desktop-core/package.json` (2 hunks)
* `scripts/list-outdated-dependencies/qa-dependencies.txt` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* scripts/list-outdated-dependencies/qa-dependencies.txt
* packages/suite-desktop-core/.example.env

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (6)</summary>

* packages/suite-desktop-core/e2e/support/testExtends/suiteBaseFixture.ts
* packages/suite-desktop-core/package.json
* packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
* packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
* packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
* packages/suite-desktop-core/e2e/support/reporters/types.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts (3)</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts:1-1
Timestamp: 2025-04-25T06:39:20.248Z
Learning: The `@octokit/rest` package should be imported as a type-only dependency in files that need just the Octokit type, using `import type { Octokit } from '@octokit/rest';` to prevent runtime ESM resolution errors. This is especially important since the actual implementation is dynamically imported at runtime in GitHubReporter.ts.
```

</details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>docs/tests/e2e-playwright-github-reporter.md</summary>

[grammar] ~42-~42: It appears that a hyphen is missing in the noun “To-do” (= task) or did you mean the verb “to do”?
Context: ...tadata:  -   **Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc...

(TO_DO_HYPHEN)

---

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ...erKeys in getterByKey  To add or change value of annotation, simply change or add new...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

---

[uncategorized] ~115-~115: You might be missing the article “an” here.
Context: ... getterByKey  To add or change value of annotation, simply change or add new enum value in...

(AI_EN_LECTOR_MISSING_DETERMINER_AN)

---

[uncategorized] ~115-~115: You might be missing the article “a” here.
Context: ...lue of annotation, simply change or add new enum value in corresponding object in `...

(AI_EN_LECTOR_MISSING_DETERMINER_A)

---

[uncategorized] ~115-~115: You might be missing the article “the” here.
Context: ... simply change or add new enum value in corresponding object in `testAnnotations.ts` 

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (13)</summary>

* GitHub Check: Socket Security: Pull Request Alerts
* GitHub Check: Socket Security: Project Report
* GitHub Check: PR-check   / node-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: PR-check   / node-override init-api-flaky
* GitHub Check: PR-check   / web-override init-api-flaky
* GitHub Check: PR-check   / web-authorizeCoinjoin cancelCoinjoinAuthorization passphrase unlockPath setBusy checkFirmwareAuthenticity keepSession cancel.test info.test resetDevice-api
* GitHub Check: build-deploy
* GitHub Check: build-deploy
* GitHub Check: Analyze with CodeQL (javascript)
* GitHub Check: transport-e2e-test
* GitHub Check: test
* GitHub Check: connect-web-e2e
* GitHub Check: Setup and Cache Dependencies

</details>

<details>
<summary>🔇 Additional comments (8)</summary><blockquote>

<details>
<summary>packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts (2)</summary>

`9-16`: **Externalise hard-coded identifiers via env vars (or CI inputs).**  

`ORGANIZATION`, `ORG_ID`, `QA_TEAM_ID`, and especially `PROJECT_NAME` are baked into the source.  
While the TODO comment hints at future work, keeping them hard-coded in an executable file:

* prevents multi-tenant / fork reuse,
* forces a source-code change for every new release project (risk for merge conflicts),
* leaks internal IDs to the public repo history.

A lightweight improvement is to read from `process.env` with sensible fall-backs; the follow-up workflow PR can then inject the correct values at runtime.

```diff
-const PROJECT_NAME = 'Test Results 37'; // TODO: Get this from CI, probably build name
+const PROJECT_NAME = process.env.PROJECT_NAME ?? 'Test Results (local)'; // CI should override
```

Same pattern can be applied to the other constants.  
[ suggest_essential_refactor ]

---

`52-66`: **Retry window may still race on parallel CI – consider exponential back-off**  

`init()` mitigates parallel‐workflow clashes by *re-querying* after `createProject()`.  
However, both jobs can still reach `createProject()` simultaneously, producing two projects before either sees the other. A second deterministic pick is fine, but GitHub will retain the orphan board.

Two suggestions:

1. Detect & delete “fresh duplicate” projects that you didn’t select (requires additional permission).
2. Use a short exponential back-off between retries around `createProject()` plus an `if-exists` guard (GraphQL mutation returns error if title already exists).

This keeps the project list tidy and avoids confusing QA with nearly identical boards.  
[ suggest_optional_refactor ][ request_verification ]

</details>
<details>
<summary>docs/tests/e2e-playwright-github-reporter.md (2)</summary>

`42-46`: **Nit-pick: hyphenate “To-do” for clarity**  

In the *Status* bullet the noun should normally be written “To-do”. Optional but improves readability.

```diff
-**Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc.)
+**Status** - Test outcome (To-do, In Progress, Done PASS, Done FAIL, etc.)
```
[ suggest_nitpick ]

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[grammar] ~42-~42: It appears that a hyphen is missing in the noun “To-do” (= task) or did you mean the verb “to do”?
Context: ...tadata:  -   **Status** - Test outcome (Todo, In Progress, Done PASS, Done FAIL, etc...

(TO_DO_HYPHEN)

</details>

</details>

---

`82-85`: **Doc / code mismatch for default `PROJECT_NAME`**  

The docs say the default is “Test Results 35”, yet `gitHubProject.ts` hard-codes “Test Results 37”.  
Please align the examples so newcomers do not chase phantom configuration issues.  
[ request_verification ][ suggest_nitpick ]

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/annotations.ts (1)</summary>

`143-145`: **`isManual` check uses non-existent `test.tags` property**  

`TestCase` (Playwright reporter API) holds tags inside `annotations` with `type === 'tag'`.  
`test.tags` isn’t defined and will always be `undefined`, so every test is treated as automated.

```diff
-    return this.test.tags.some(tag => tag.startsWith('@group=manual'));
+    return this.test.annotations.some(
+        a => a.type === 'tag' && a.description?.startsWith('@group=manual'),
+    );
```

This fix restores correct classification and prevents manual tests from being mis-labelled “Automated”.  
[ flag_critical_issue ]

</details>
<details>
<summary>packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts (3)</summary>

`96-105`: **Well-structured BaseAnnotation interface**

The BaseAnnotation interface provides a solid foundation for standardizing annotation metadata. The inclusion of optional properties adds flexibility while maintaining type safety.

---

`68-86`: **Good implementation of test status tracking**

The TestStatus enum and corresponding color mapping provide a comprehensive way to track test lifecycle states. Including both manual and automated outcomes with distinct visual indicators is a great approach for at-a-glance status reporting.

---

`52-58`: <details>
<summary>✅ Verification successful</summary>

**Verify color format compatibility with GitHub API**

The colors are defined as uppercase strings ('RED', 'ORANGE', etc.) for test priorities. Ensure these color values are compatible with GitHub's API which typically expects either hex codes or specific color names. Consider documenting the expected format or adding a mapping function if GitHub requires different values.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check how these color strings are processed before sending to GitHub API
grep -r "TestPriorityColors\|TestStatusColors" --include="*.ts" . | grep -v "testAnnotations.ts"
```

Length of output: 96

---

I want to confirm where and how these colors are used. Let's search the entire repo for references to the enum, its mapping, and imports:

---

🏁 Script executed:

```shell
#!/bin/bash
# Find any usage of the color map or enum
grep -R "TestPriorityColors" -n .
grep -R "TestPriority" -n .
grep -R "testAnnotations" -n .
```

Length of output: 29601

---

Let's inspect how the GitHub reporter uses these color values when creating project field options:

---

🏁 Script executed:

```shell
#!/bin/bash
# Show GraphQL mutations and color usage in the GitHub reporter
grep -nE 'addProjectV2FieldOption|createProjectV2FieldOption|ProjectV2FieldSingleSelectOptionColor' packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
grep -n 'color:' packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
```

Length of output: 252

---

To see exactly how these color strings flow through your annotations layer into the GitHub API calls, let’s dump both the project-field mapping in `testAnnotations.ts` and the GraphQL mutation in the reporter:

---

🏁 Script executed:

```shell
#!/bin/bash
echo "=== annotationsForProjectFields in testAnnotations.ts ==="
grep -R "annotationsForProjectFields" -n packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts
sed -n '140,260p' packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts

echo -e "\n=== GraphQL mutation in gitHubProject.ts ==="
grep -R "mutation" -n packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts || true
sed -n '1,200p' packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
```

Length of output: 8620

---

Let’s inspect `projectRequests.ts` to see the exact GraphQL mutations and how color values are passed:

---

🏁 Script executed:

```shell
#!/bin/bash
grep -n "createProjectField" -n packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
grep -n "updateFieldOptions" -n packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
sed -n '1,200p' packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
sed -n '200,350p' packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
```

Length of output: 5977

---

Let's inspect the GraphQL enum for option colors in our type definitions:

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for the GraphQL enum definition in our types file
grep -R "enum ProjectV2SingleSelectOptionColor" -n packages/suite-desktop-core/e2e/support/reporters/types.ts
sed -n '1,200p' packages/suite-desktop-core/e2e/support/reporters/types.ts
sed -n '200,400p' packages/suite-desktop-core/e2e/support/reporters/types.ts
```

Length of output: 1855

---



</details>

**TestPriorityColors correctly use GitHub GraphQL enum values – no changes needed**

The `TestPriorityColors` map (RED, ORANGE, YELLOW, GREEN, GRAY) is passed unquoted into the GraphQL mutations as enum values matching GitHub’s ProjectV2SingleSelectOptionColor. Existing values are valid, and the code already embeds them correctly:

• packages/suite-desktop-core/e2e/support/enums/testAnnotations.ts:52-58

If you introduce new priority levels, just be sure their string keys match the GraphQL enum.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-25T19:03:47Z
user: login: Vere-Grey
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T19:29:43Z
user: login: HajekOndrej
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T19:59:15Z

Review comments:
diff_hunk: @@ -0,0 +1,280 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    ProjectQueryResponseUser,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
+
+    async createProjectField(
+        projectId: string,
+        fieldName: string,
+        fieldType: string,
+        options?: { value: string; color: string }[],
+    ): Promise<string> {
+        const isSelectField = fieldType === 'SINGLE_SELECT';
+        let optionsString = '';
+        this.logger.log(
+            `Creating ${fieldType} field "${fieldName}" in project ${projectId}${
+                isSelectField ? ` with ${options!.length} options` : ''
+            }...`,
+        );
+
+        if (isSelectField && options) {
+            optionsString = options
+                .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+                .join(',\n');
+        }
+
+        const mutation = `
+      mutation {
+        createProjectV2Field(
+        input: {
+          projectId: "${projectId}"
+          name: "${fieldName}"
+          dataType: ${fieldType}
+          ${isSelectField ? `singleSelectOptions: [${optionsString}]` : ''}
+        }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+        } 
+      }`;
+
+        const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
+        const fieldId = response.createProjectV2Field.projectV2Field.id;
+        this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
+
+        return fieldId;
+    }
+
+    async getProjectFromOrganization(
+        organization: string,
+        projectName: string,
+    ): Promise<Project[]> {
+        this.logger.log(`Fetching projects for organization: ${organization}`);
+
+        const query = `
+            query {
+              organization(login: "${organization}") {
+                projectsV2(first: 10, query: "${projectName}") {
+                  nodes {
+                    id
+                    title
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponse>(query);
+        this.logger.log('Successfully retrieved organization projects');
+
+        return response.organization.projectsV2.nodes;
+    }
+
+    async getProjectFromUser(ownerLogin: string, projectName: string): Promise<Project[]> {
+        this.logger.log(`Fetching projects for user: ${ownerLogin}`);
+
+        const query = `
+            query {
+              user(login: "${ownerLogin}") {
+                projectsV2(first: 10, query: "${projectName}") {
+                  nodes {
+                    id
+                    title
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponseUser>(query);
+        this.logger.log('Successfully retrieved user projects');
+
+        return response.user.projectsV2.nodes;
+    }
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    async getProjectFields(projectId: string): Promise<ProjectField[]> {
+        this.logger.log(`Fetching fields for project ${projectId}...`);
+
+        const response = await this.octokit.graphql<ProjectFieldsResponse>(`
+          query {
+            node(id: "${projectId}") {
+              ... on ProjectV2 {
+                fields(first: 20) {
+                  nodes {
+                    ... on ProjectV2FieldCommon {
+                      id
+                      name
+                      dataType
+                    }
+                    ... on ProjectV2SingleSelectField {
+                      id
+                      name
+                      dataType
+                      options {
+                        id
+                        name
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+      `);
+
+        this.logger.log(`Successfully retrieved fields for project ${projectId}`);
+
+        return response.node.fields.nodes;
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async updateFieldOptions(
+        fieldId: string,
+        options: { value: string; color: string }[],
+    ): Promise<void> {
+        this.logger.log(`Updating all options for field ${fieldId}`);
+
+        const optionsString = options
+            .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+            .join(',\n');
+
+        const mutation = `
+        mutation {
+          updateProjectV2Field(
+          input: {
+            fieldId: "${fieldId}"
+            singleSelectOptions: [
+              ${optionsString}
+            ]
+          }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+          } 
+        }
+      `;
+
+        await this.octokit.graphql(mutation);
+        this.logger.log(`Successfully replaced all options for field ${fieldId}`);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: github-advanced-security[bot]
body: ## Incomplete string escaping or encoding

This does not escape backslash characters in the input.

[Show more details](https://github.com/trezor/trezor-suite/security/code-scanning/180)
created_at: 2025-04-23T08:36:12Z
author_association: NONE
diff_hunk: @@ -0,0 +1,280 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    ProjectQueryResponseUser,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
+
+    async createProjectField(
+        projectId: string,
+        fieldName: string,
+        fieldType: string,
+        options?: { value: string; color: string }[],
+    ): Promise<string> {
+        const isSelectField = fieldType === 'SINGLE_SELECT';
+        let optionsString = '';
+        this.logger.log(
+            `Creating ${fieldType} field "${fieldName}" in project ${projectId}${
+                isSelectField ? ` with ${options!.length} options` : ''
+            }...`,
+        );
+
+        if (isSelectField && options) {
+            optionsString = options
+                .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+                .join(',\n');
+        }
+
+        const mutation = `
+      mutation {
+        createProjectV2Field(
+        input: {
+          projectId: "${projectId}"
+          name: "${fieldName}"
+          dataType: ${fieldType}
+          ${isSelectField ? `singleSelectOptions: [${optionsString}]` : ''}
+        }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+        } 
+      }`;
+
+        const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
+        const fieldId = response.createProjectV2Field.projectV2Field.id;
+        this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
+
+        return fieldId;
+    }
+
+    async getProjectFromOrganization(
+        organization: string,
+        projectName: string,
+    ): Promise<Project[]> {
+        this.logger.log(`Fetching projects for organization: ${organization}`);
+
+        const query = `
+            query {
+              organization(login: "${organization}") {
+                projectsV2(first: 10, query: "${projectName}") {
+                  nodes {
+                    id
+                    title
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponse>(query);
+        this.logger.log('Successfully retrieved organization projects');
+
+        return response.organization.projectsV2.nodes;
+    }
+
+    async getProjectFromUser(ownerLogin: string, projectName: string): Promise<Project[]> {
+        this.logger.log(`Fetching projects for user: ${ownerLogin}`);
+
+        const query = `
+            query {
+              user(login: "${ownerLogin}") {
+                projectsV2(first: 10, query: "${projectName}") {
+                  nodes {
+                    id
+                    title
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponseUser>(query);
+        this.logger.log('Successfully retrieved user projects');
+
+        return response.user.projectsV2.nodes;
+    }
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    async getProjectFields(projectId: string): Promise<ProjectField[]> {
+        this.logger.log(`Fetching fields for project ${projectId}...`);
+
+        const response = await this.octokit.graphql<ProjectFieldsResponse>(`
+          query {
+            node(id: "${projectId}") {
+              ... on ProjectV2 {
+                fields(first: 20) {
+                  nodes {
+                    ... on ProjectV2FieldCommon {
+                      id
+                      name
+                      dataType
+                    }
+                    ... on ProjectV2SingleSelectField {
+                      id
+                      name
+                      dataType
+                      options {
+                        id
+                        name
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+      `);
+
+        this.logger.log(`Successfully retrieved fields for project ${projectId}`);
+
+        return response.node.fields.nodes;
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async updateFieldOptions(
+        fieldId: string,
+        options: { value: string; color: string }[],
+    ): Promise<void> {
+        this.logger.log(`Updating all options for field ${fieldId}`);
+
+        const optionsString = options
+            .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+            .join(',\n');
+
+        const mutation = `
+        mutation {
+          updateProjectV2Field(
+          input: {
+            fieldId: "${fieldId}"
+            singleSelectOptions: [
+              ${optionsString}
+            ]
+          }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+          } 
+        }
+      `;
+
+        await this.octokit.graphql(mutation);
+        this.logger.log(`Successfully replaced all options for field ${fieldId}`);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
+              body: "${body.replace(/"/g, '\\"')}"
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: github-advanced-security[bot]
body: ## Incomplete string escaping or encoding

This does not escape backslash characters in the input.

[Show more details](https://github.com/trezor/trezor-suite/security/code-scanning/181)
created_at: 2025-04-23T08:36:12Z
author_association: NONE
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Make log level & project name configurable instead of hard-coded**

Hard-coding `PROJECT_NAME` and `VERBOSE` limits reuse across branches and noisy logs in CI.

```diff
-const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
-const VERBOSE = true;
+const PROJECT_NAME = process.env.PROJECT_NAME ?? 'Test Results';
+const VERBOSE = (process.env.VERBOSE ?? 'false').toLowerCase() === 'true';
```

This keeps behaviour identical for local runs while allowing CI to override.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
-const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
-const VERBOSE = true;
+const PROJECT_NAME = process.env.PROJECT_NAME ?? 'Test Results';
+const VERBOSE = (process.env.VERBOSE ?? 'false').toLowerCase() === 'true';
 const RETRY_CONF = {
     attempts: 3,
     gap: 500,
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-23T21:56:54Z
author_association: NONE
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Duplicate import of `@octokit/rest`**

`Octokit` is statically imported here, yet a dynamic import is performed again in `onBegin()`. This loads the same module twice and defeats the purpose of dynamic loading.  Prefer one approach:

```diff
-import { Octokit } from '@octokit/rest';
+// Remove static import – instance is loaded dynamically in onBegin()
```
Or drop the dynamic `import()` and keep the static import if bundle size isn’t a concern.

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commit abd928d

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-23T21:56:54Z
author_association: NONE
diff_hunk: @@ -14,53 +24,177 @@ type TestMetadataInput = {
     stream?: TestStream;
 };
 
-export const formatTestSteps = (steps: string[]): string =>
+const formatList = (steps: string[]): string =>
     steps.map((step, index) => `${index + 1}. ${step}`).join('\n');
 
-export const createTestAnnotation = (metadata: TestMetadataInput) => {
+// Loops thru params and adds these metadata to the test as annotation, used in test files
+export const createTestAnnotation = (metadata: TestMetadataInput): TestDetailsAnnotation[] => {
     const formattedAnnotations = [];
 
-    if (metadata.testCase) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.TestCase,
-            description: metadata.testCase,
-        });
+    for (const [key, value] of Object.entries(metadata)) {
+        const annotation = annotationsAddedToTest.find(a => a.key === key);
+        if (!value || !annotation?.annotationType) {
+            continue;
+        }
+
+        const type = annotation.annotationType.toString();
+        if (annotation.needsFormatting) {
+            formattedAnnotations.push({ type, description: formatList(value as string[]) });
+        } else {
+            formattedAnnotations.push({ type, description: value as string });
+        }
     }
 
-    if (metadata.prerequisites) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Prerequisites,
-            description: formatTestSteps(metadata.prerequisites),
-        });
+    return formattedAnnotations;
+};
+
+// Class used by our GitHub Reporter to extract metadata from the test and its run
+export class TestReportProvider {
+    private readonly test: TestCase;
+    private readonly annotationMap: Map<string, string>;
+    private readonly defaults = {
+        prerequisites: 'No prerequisites defined',
+        steps: 'No steps defined',
+        category: TestCategory.NotCategorized,
+        priority: TestPriority.Medium,
+        stream: TestStream.NotDefined,
+        deviceModel: DeviceModel.Unknown,
+    };
+
+    constructor(test: TestCase) {
+        this.test = test;
+        this.annotationMap = new Map();
+
+        for (const annotation of test.annotations) {
+            if (!annotation.description) {
+                continue;
+            }
+            this.annotationMap.set(annotation.type, annotation.description);
+        }
     }
 
-    if (metadata.steps) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Steps,
-            description: formatTestSteps(metadata.steps),
-        });
+    private getAnnotation(type: string, defaultValue: string): string {
+        return this.annotationMap.has(type) ? this.annotationMap.get(type)! : defaultValue;
     }
 
-    if (metadata.category) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Category,
-            description: metadata.category,
-        });
+    get testCase(): string {
+        return this.getAnnotation(TestAnnotationType.TestCase, this.test.title);
     }
+    get status(): string {
+        // This condition covers manual and automated tests that are skipped
+        if (this.test.outcome() === 'skipped') {
+            return TestStatus.Todo;
+        }
+
+        if (this.test.ok()) {
+            return TestStatus.AutoPass;
+        }
 
-    if (metadata.priority) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Priority,
-            description: metadata.priority,
-        });
+        if (!this.test.ok()) {
+            return TestStatus.AutoFail;
+        }
+
+        return TestStatus.Todo;
     }
 
-    if (metadata.stream) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Stream,
-            description: metadata.stream,
-        });
+    get prerequisites(): string {
+        return this.getAnnotation(TestAnnotationType.Prerequisites, this.defaults.prerequisites);
     }
 
-    return formattedAnnotations;
-};
+    get steps(): string {
+        return this.getAnnotation(TestAnnotationType.Steps, this.defaults.steps);
+    }
+
+    get category(): string {
+        return this.getAnnotation(TestAnnotationType.Category, this.defaults.category);
+    }
+
+    get priority(): string {
+        return this.getAnnotation(TestAnnotationType.Priority, this.defaults.priority);
+    }
+
+    get stream(): string {
+        return this.getAnnotation(TestAnnotationType.Stream, this.defaults.stream);
+    }
+
+    get testRun(): string {
+        if (this.isManual) {
+            return 'Manual';
+        } else {
+            // Web or Desktop
+            const projectType = this.test.parent.project()?.name;
+
+            return capitalizeFirstLetter(projectType ?? 'Automated');
+        }
+    }
+
+    get deviceModel(): string {
+        return this.getAnnotation(TestAnnotationType.DeviceModel, this.defaults.deviceModel);
+    }
+
+    get comment(): string {
+        return '';
+    }
+
+    get rawAnnotations(): Array<{ type: string; description?: string }> {
+        return this.test.annotations;
+    }
+
+    get isManual(): boolean {
+        return this.test.tags.some(tag => tag.startsWith('@group=manual'));
+    }
+
+    get bodyDescription(): string {
+        const sections = [];
+
+        if (this.isManual) {
+            for (const annotation of annotationsForBodyDescription) {
+                const value = this.getterByKey(annotation.key);
+                sections.push(`## ${annotation.name}\n${value}`);
+            }
+        } else {
+            sections.push('## Automated Test');
+        }
+
+        return sections.join('\n---\n');
+    }
+
+    get projectValues(): Array<{ name: string; value: string }> {
+        return annotationsForProjectFields.map(field => ({
+            name: field.name,
+            value: this.getterByKey(field.key),
+        }));
+    }
+
+    // This method allow us to loop thru array of keys and get the value from the getter
+    // That way `bodyDescription` and `projectValues` can be generated dynamically
+    // and rely on annotations objects (ex: 'annotationsForBodyDescription') as single source definitions
+    getterByKey(key: string): string {
+        // This is the downside, we need to list all our annotation getters here
+        const validGetterKeys = [
+            'testCase',
+            'prerequisites',
+            'steps',
+            'category',
+            'priority',
+            'stream',
+            'status',
+            'testRun',
+            'deviceModel',
+            'comment',
+        ] as const;
+
+        type StringGetterKey = (typeof validGetterKeys)[number];
+
+        const isValidKey = (k: string): k is StringGetterKey =>
+            validGetterKeys.includes(k as StringGetterKey);
+
+        if (!isValidKey(key)) {
+            throw new Error(
+                `The key '${key}' does not have corresponding getter on TestReportProvider class.`,
+            );
+        }
+
+        return this[key];
+    }
path: packages/suite-desktop-core/e2e/support/annotations.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Getter mapping could be made type-safe**

Using `this[key]` on an arbitrary string forfeits type checking. A keyed lookup via a typed map avoids runtime errors and removes the hard-coded `validGetterKeys` list.

```diff
-const validGetterKeys = [...]
-...
-return this[key];
+const getters: Record<string, () => string> = {
+    testCase: () => this.testCase,
+    prerequisites: () => this.prerequisites,
+    steps: () => this.steps,
+    category: () => this.category,
+    priority: () => this.priority,
+    stream: () => this.stream,
+    status: () => this.status,
+    testRun: () => this.testRun,
+    deviceModel: () => this.deviceModel,
+    comment: () => this.comment,
+};
+
+const getter = getters[key];
+if (!getter) {
+    throw new Error(`Getter for '${key}' is not defined.`);
+}
+return getter();
```
This removes string-indexing on `this` and scales better as new keys are added.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    // This method allow us to loop thru array of keys and get the value from the getter
    // That way `bodyDescription` and `projectValues` can be generated dynamically
    // and rely on annotations objects (ex: 'annotationsForBodyDescription') as single source definitions
    getterByKey(key: string): string {
        const getters: Record<string, () => string> = {
            testCase:    () => this.testCase,
            prerequisites: () => this.prerequisites,
            steps:       () => this.steps,
            category:    () => this.category,
            priority:    () => this.priority,
            stream:      () => this.stream,
            status:      () => this.status,
            testRun:     () => this.testRun,
            deviceModel: () => this.deviceModel,
            comment:     () => this.comment,
        };

        const getter = getters[key];
        if (!getter) {
            throw new Error(`Getter for '${key}' is not defined.`);
        }
        return getter();
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commit abd928d

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-23T21:56:54Z
author_association: NONE
diff_hunk: @@ -0,0 +1,258 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling for GraphQL operations**

The method doesn't implement error handling for GraphQL operations, which could lead to unhandled promise rejections.


Consider adding try/catch blocks to all GraphQL operations:

```diff
async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
    this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);

    const mutation = `
        mutation {
            createProjectV2(
            input: {
                ownerId: "${ownerId}"
                title: "${projectName}"
                teamId: "${teamId}"
            }) {
                projectV2 {
                    id
                }
            }
        }
    `;

+   try {
        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
        const projectId = response.createProjectV2.projectV2.id;

        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);

        return projectId;
+   } catch (error) {
+       this.logger.logError(`Failed to create project "${projectName}":`, error);
+       throw error;
+   }
}
```

Apply similar error handling to all methods in this class.


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);

        const mutation = `
            mutation {
                createProjectV2(
                input: {
                    ownerId: "${ownerId}"
                    title: "${projectName}"
                    teamId: "${teamId}"
                }) {
                    projectV2 {
                        id
                    }
                }
            }
        `;

        try {
            const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
            const projectId = response.createProjectV2.projectV2.id;

            this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);

            return projectId;
        } catch (error) {
            this.logger.logError(`Failed to create project "${projectName}":`, error);
            throw error;
        }
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-23T21:56:54Z
author_association: NONE
diff_hunk: @@ -14,53 +24,177 @@ type TestMetadataInput = {
     stream?: TestStream;
 };
 
-export const formatTestSteps = (steps: string[]): string =>
+const formatList = (steps: string[]): string =>
     steps.map((step, index) => `${index + 1}. ${step}`).join('\n');
 
-export const createTestAnnotation = (metadata: TestMetadataInput) => {
+// Loops thru params and adds these metadata to the test as annotation, used in test files
+export const createTestAnnotation = (metadata: TestMetadataInput): TestDetailsAnnotation[] => {
     const formattedAnnotations = [];
 
-    if (metadata.testCase) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.TestCase,
-            description: metadata.testCase,
-        });
+    for (const [key, value] of Object.entries(metadata)) {
+        const annotation = annotationsAddedToTest.find(a => a.key === key);
+        if (!value || !annotation?.annotationType) {
+            continue;
+        }
+
+        const type = annotation.annotationType.toString();
+        if (annotation.needsFormatting) {
+            formattedAnnotations.push({ type, description: formatList(value as string[]) });
+        } else {
+            formattedAnnotations.push({ type, description: value as string });
+        }
     }
 
-    if (metadata.prerequisites) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Prerequisites,
-            description: formatTestSteps(metadata.prerequisites),
-        });
+    return formattedAnnotations;
+};
+
+// Class used by our GitHub Reporter to extract metadata from the test and its run
+export class TestReportProvider {
+    private readonly test: TestCase;
+    private readonly annotationMap: Map<string, string>;
+    private readonly defaults = {
+        prerequisites: 'No prerequisites defined',
+        steps: 'No steps defined',
+        category: TestCategory.NotCategorized,
+        priority: TestPriority.Medium,
+        stream: TestStream.NotDefined,
+        deviceModel: DeviceModel.Unknown,
+    };
+
+    constructor(test: TestCase) {
+        this.test = test;
+        this.annotationMap = new Map();
+
+        for (const annotation of test.annotations) {
+            if (!annotation.description) {
+                continue;
+            }
+            this.annotationMap.set(annotation.type, annotation.description);
+        }
     }
 
-    if (metadata.steps) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Steps,
-            description: formatTestSteps(metadata.steps),
-        });
+    private getAnnotation(type: string, defaultValue: string): string {
+        return this.annotationMap.has(type) ? this.annotationMap.get(type)! : defaultValue;
     }
 
-    if (metadata.category) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Category,
-            description: metadata.category,
-        });
+    get testCase(): string {
+        return this.getAnnotation(TestAnnotationType.TestCase, this.test.title);
     }
+    get status(): string {
+        // This condition covers manual and automated tests that are skipped
+        if (this.test.outcome() === 'skipped') {
+            return TestStatus.Todo;
+        }
+
+        if (this.test.ok()) {
+            return TestStatus.AutoPass;
+        }
 
-    if (metadata.priority) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Priority,
-            description: metadata.priority,
-        });
+        if (!this.test.ok()) {
+            return TestStatus.AutoFail;
+        }
+
+        return TestStatus.Todo;
     }
 
-    if (metadata.stream) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Stream,
-            description: metadata.stream,
-        });
+    get prerequisites(): string {
+        return this.getAnnotation(TestAnnotationType.Prerequisites, this.defaults.prerequisites);
     }
 
-    return formattedAnnotations;
-};
+    get steps(): string {
+        return this.getAnnotation(TestAnnotationType.Steps, this.defaults.steps);
+    }
+
+    get category(): string {
+        return this.getAnnotation(TestAnnotationType.Category, this.defaults.category);
+    }
+
+    get priority(): string {
+        return this.getAnnotation(TestAnnotationType.Priority, this.defaults.priority);
+    }
+
+    get stream(): string {
+        return this.getAnnotation(TestAnnotationType.Stream, this.defaults.stream);
+    }
+
+    get testRun(): string {
+        if (this.isManual) {
+            return 'Manual';
+        } else {
+            // Web or Desktop
+            const projectType = this.test.parent.project()?.name;
+
+            return capitalizeFirstLetter(projectType ?? 'Automated');
+        }
+    }
+
+    get deviceModel(): string {
+        return this.getAnnotation(TestAnnotationType.DeviceModel, this.defaults.deviceModel);
+    }
+
+    get comment(): string {
+        return '';
+    }
+
+    get rawAnnotations(): Array<{ type: string; description?: string }> {
+        return this.test.annotations;
+    }
+
+    get isManual(): boolean {
+        return this.test.tags.some(tag => tag.startsWith('@group=manual'));
+    }
+
+    get bodyDescription(): string {
+        const sections = [];
+
+        if (this.isManual) {
+            for (const annotation of annotationsForBodyDescription) {
+                const value = this.getterByKey(annotation.key);
+                sections.push(`## ${annotation.name}\n${value}`);
+            }
+        } else {
+            sections.push('## Automated Test');
+        }
+
+        return sections.join('\n---\n');
+    }
+
+    get projectValues(): Array<{ name: string; value: string }> {
+        return annotationsForProjectFields.map(field => ({
+            name: field.name,
+            value: this.getterByKey(field.key),
+        }));
+    }
+
+    // This method allow us to loop thru array of keys and get the value from the getter
+    // That way `bodyDescription` and `projectValues` can be generated dynamically
+    // and rely on annotations objects (ex: 'annotationsForBodyDescription') as single source definitions
+    getterByKey(key: string): string {
+        // This is the downside, we need to list all our annotation getters here
+        const validGetterKeys = [
+            'testCase',
+            'prerequisites',
+            'steps',
+            'category',
+            'priority',
+            'stream',
+            'status',
+            'testRun',
+            'deviceModel',
+            'comment',
+        ] as const;
+
+        type StringGetterKey = (typeof validGetterKeys)[number];
+
+        const isValidKey = (k: string): k is StringGetterKey =>
+            validGetterKeys.includes(k as StringGetterKey);
+
+        if (!isValidKey(key)) {
+            throw new Error(
+                `The key '${key}' does not have corresponding getter on TestReportProvider class.`,
+            );
+        }
+
+        return this[key];
+    }
path: packages/suite-desktop-core/e2e/support/annotations.ts
user: login: HajekOndrej
body: This seems valid.
created_at: 2025-04-24T13:17:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,258 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: HajekOndrej
body: I am not sure this is aptly named. I think it's not exactly a client. Maybe more of a wrapper for a client?
created_at: 2025-04-24T13:24:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,258 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: HajekOndrej
body: It is also huge and maybe some of the methods don't even handle project stuff directly, so potential for splitting to more single responsibility classes?
created_at: 2025-04-24T13:25:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: HajekOndrej
body: I agree that this underscore codestyle is good, but we don't use it anywhere else, right?
created_at: 2025-04-24T13:26:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: HajekOndrej
body: And even if we did, it seems inconsistent across the file.
created_at: 2025-04-24T13:26:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: HajekOndrej
body: Suspicous: What if this is not set? The rest of this operation will probably just fail? Maybe there should be return in the else block?
Also does this check need to be part of the trackOperation block? I think it could be moved above.
created_at: 2025-04-24T13:31:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: HajekOndrej
body: what does this scheduleAction method do?
created_at: 2025-04-24T13:32:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
+                        this.log(`Creating GitHub draft issue for test "${test.title}"...`);
+
+                        return this.graphQLClient.createDraftIssueInProject(
+                            this.projectId,
+                            report.testCase,
+                            report.bodyDescription,
+                        );
+                    }, RETRY_CONF);
+                    this.log(`[${issueNodeId}] Successfully created issue "${test.title}"`);
+
+                    const fields = await scheduleAction(() => this.getProjectFields(), RETRY_CONF);
+
+                    for (const { name, value } of report.projectValues) {
+                        const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(
+                            fields,
+                            name,
+                            value,
+                        );
+                        await scheduleAction(() => {
+                            this.log(`[${issueNodeId}] Updating field ${name}:"${value}"...`);
+
+                            return this.graphQLClient.setItemValue(
+                                this.projectId,
+                                issueNodeId,
+                                fieldId,
+                                valueOrOptionId,
+                            );
+                        }, RETRY_CONF);
+                        this.log(`[${issueNodeId}] Successfully updated field ${name}:"${value}"`);
+                    }
+
+                    this.log(
+                        `[${issueNodeId}] Successfully recorded test result for "${test.title}"`,
+                    );
+                } catch (error) {
+                    this.logError(`Failed to process test end for "${test.title}":`, error);
+                    // Non-Critical error, no need to rethrow
+                }
+            })(),
+        );
+    }
+
+    // Finalizes reporting when all tests are complete, waits for pending operations to finish
+    // eslint-disable-next-line require-await
+    async onEnd() {
+        this.log('All tests completed, waiting for pending operations...');
+
+        if (this.pendingOperations.length > 0) {
+            this.log(`Waiting for ${this.pendingOperations.length} pending operations to complete`);
+
+            return Promise.allSettled(this.pendingOperations)
+                .then(results => {
+                    const failed = results.filter(r => r.status === 'rejected').length;
+                    if (failed > 0) {
+                        this.logError(`${failed} operations failed`);
+                    } else {
+                        this.log('All operations completed successfully');
+                    }
+                })
+                .finally(() => {
+                    this.log('GitHub reporter finished');
+                });
+        } else {
+            this.log('No pending operations, GitHub reporter finished');
+
+            return Promise.resolve();
+        }
+    }
+
+    private async initializeProject(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.log(`Using created project: ${createdProject.title} (${createdProject.id})`);
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
+        try {
+            const projects = await this.graphQLClient.getProjectFromOrganization(
+                ORGANIZATION,
+                PROJECT_NAME,
+            );
+
+            const matchingProject = projects.find((project: any) => project.title === PROJECT_NAME);
+
+            if (matchingProject) {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: HajekOndrej
body: I suggest reversing this logic for better readability.
created_at: 2025-04-24T13:35:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
+                        this.log(`Creating GitHub draft issue for test "${test.title}"...`);
+
+                        return this.graphQLClient.createDraftIssueInProject(
+                            this.projectId,
+                            report.testCase,
+                            report.bodyDescription,
+                        );
+                    }, RETRY_CONF);
+                    this.log(`[${issueNodeId}] Successfully created issue "${test.title}"`);
+
+                    const fields = await scheduleAction(() => this.getProjectFields(), RETRY_CONF);
+
+                    for (const { name, value } of report.projectValues) {
+                        const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(
+                            fields,
+                            name,
+                            value,
+                        );
+                        await scheduleAction(() => {
+                            this.log(`[${issueNodeId}] Updating field ${name}:"${value}"...`);
+
+                            return this.graphQLClient.setItemValue(
+                                this.projectId,
+                                issueNodeId,
+                                fieldId,
+                                valueOrOptionId,
+                            );
+                        }, RETRY_CONF);
+                        this.log(`[${issueNodeId}] Successfully updated field ${name}:"${value}"`);
+                    }
+
+                    this.log(
+                        `[${issueNodeId}] Successfully recorded test result for "${test.title}"`,
+                    );
+                } catch (error) {
+                    this.logError(`Failed to process test end for "${test.title}":`, error);
+                    // Non-Critical error, no need to rethrow
+                }
+            })(),
+        );
+    }
+
+    // Finalizes reporting when all tests are complete, waits for pending operations to finish
+    // eslint-disable-next-line require-await
+    async onEnd() {
+        this.log('All tests completed, waiting for pending operations...');
+
+        if (this.pendingOperations.length > 0) {
+            this.log(`Waiting for ${this.pendingOperations.length} pending operations to complete`);
+
+            return Promise.allSettled(this.pendingOperations)
+                .then(results => {
+                    const failed = results.filter(r => r.status === 'rejected').length;
+                    if (failed > 0) {
+                        this.logError(`${failed} operations failed`);
+                    } else {
+                        this.log('All operations completed successfully');
+                    }
+                })
+                .finally(() => {
+                    this.log('GitHub reporter finished');
+                });
+        } else {
+            this.log('No pending operations, GitHub reporter finished');
+
+            return Promise.resolve();
+        }
+    }
+
+    private async initializeProject(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.log(`Using created project: ${createdProject.title} (${createdProject.id})`);
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: HajekOndrej
body: All these methods handling projects could most likely live in a different class.
created_at: 2025-04-24T13:38:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,53 +24,177 @@ type TestMetadataInput = {
     stream?: TestStream;
 };
 
-export const formatTestSteps = (steps: string[]): string =>
+const formatList = (steps: string[]): string =>
     steps.map((step, index) => `${index + 1}. ${step}`).join('\n');
 
-export const createTestAnnotation = (metadata: TestMetadataInput) => {
+// Loops thru params and adds these metadata to the test as annotation, used in test files
+export const createTestAnnotation = (metadata: TestMetadataInput): TestDetailsAnnotation[] => {
     const formattedAnnotations = [];
 
-    if (metadata.testCase) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.TestCase,
-            description: metadata.testCase,
-        });
+    for (const [key, value] of Object.entries(metadata)) {
+        const annotation = annotationsAddedToTest.find(a => a.key === key);
+        if (!value || !annotation?.annotationType) {
+            continue;
+        }
+
+        const type = annotation.annotationType.toString();
+        if (annotation.needsFormatting) {
+            formattedAnnotations.push({ type, description: formatList(value as string[]) });
+        } else {
+            formattedAnnotations.push({ type, description: value as string });
+        }
     }
 
-    if (metadata.prerequisites) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Prerequisites,
-            description: formatTestSteps(metadata.prerequisites),
-        });
+    return formattedAnnotations;
+};
+
+// Class used by our GitHub Reporter to extract metadata from the test and its run
+export class TestReportProvider {
+    private readonly test: TestCase;
+    private readonly annotationMap: Map<string, string>;
+    private readonly defaults = {
+        prerequisites: 'No prerequisites defined',
+        steps: 'No steps defined',
+        category: TestCategory.NotCategorized,
+        priority: TestPriority.Medium,
+        stream: TestStream.NotDefined,
+        deviceModel: DeviceModel.Unknown,
+    };
+
+    constructor(test: TestCase) {
+        this.test = test;
+        this.annotationMap = new Map();
+
+        for (const annotation of test.annotations) {
+            if (!annotation.description) {
+                continue;
+            }
+            this.annotationMap.set(annotation.type, annotation.description);
+        }
     }
 
-    if (metadata.steps) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Steps,
-            description: formatTestSteps(metadata.steps),
-        });
+    private getAnnotation(type: string, defaultValue: string): string {
+        return this.annotationMap.has(type) ? this.annotationMap.get(type)! : defaultValue;
     }
 
-    if (metadata.category) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Category,
-            description: metadata.category,
-        });
+    get testCase(): string {
+        return this.getAnnotation(TestAnnotationType.TestCase, this.test.title);
     }
+    get status(): string {
+        // This condition covers manual and automated tests that are skipped
+        if (this.test.outcome() === 'skipped') {
+            return TestStatus.Todo;
+        }
+
+        if (this.test.ok()) {
+            return TestStatus.AutoPass;
+        }
 
-    if (metadata.priority) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Priority,
-            description: metadata.priority,
-        });
+        if (!this.test.ok()) {
+            return TestStatus.AutoFail;
+        }
+
+        return TestStatus.Todo;
     }
 
-    if (metadata.stream) {
-        formattedAnnotations.push({
-            type: TestAnnotationType.Stream,
-            description: metadata.stream,
-        });
+    get prerequisites(): string {
+        return this.getAnnotation(TestAnnotationType.Prerequisites, this.defaults.prerequisites);
     }
 
-    return formattedAnnotations;
-};
+    get steps(): string {
+        return this.getAnnotation(TestAnnotationType.Steps, this.defaults.steps);
+    }
+
+    get category(): string {
+        return this.getAnnotation(TestAnnotationType.Category, this.defaults.category);
+    }
+
+    get priority(): string {
+        return this.getAnnotation(TestAnnotationType.Priority, this.defaults.priority);
+    }
+
+    get stream(): string {
+        return this.getAnnotation(TestAnnotationType.Stream, this.defaults.stream);
+    }
+
+    get testRun(): string {
+        if (this.isManual) {
+            return 'Manual';
+        } else {
+            // Web or Desktop
+            const projectType = this.test.parent.project()?.name;
+
+            return capitalizeFirstLetter(projectType ?? 'Automated');
+        }
+    }
+
+    get deviceModel(): string {
+        return this.getAnnotation(TestAnnotationType.DeviceModel, this.defaults.deviceModel);
+    }
+
+    get comment(): string {
+        return '';
+    }
+
+    get rawAnnotations(): Array<{ type: string; description?: string }> {
+        return this.test.annotations;
+    }
+
+    get isManual(): boolean {
+        return this.test.tags.some(tag => tag.startsWith('@group=manual'));
+    }
+
+    get bodyDescription(): string {
+        const sections = [];
+
+        if (this.isManual) {
+            for (const annotation of annotationsForBodyDescription) {
+                const value = this.getterByKey(annotation.key);
+                sections.push(`## ${annotation.name}\n${value}`);
+            }
+        } else {
+            sections.push('## Automated Test');
+        }
+
+        return sections.join('\n---\n');
+    }
+
+    get projectValues(): Array<{ name: string; value: string }> {
+        return annotationsForProjectFields.map(field => ({
+            name: field.name,
+            value: this.getterByKey(field.key),
+        }));
+    }
+
+    // This method allow us to loop thru array of keys and get the value from the getter
+    // That way `bodyDescription` and `projectValues` can be generated dynamically
+    // and rely on annotations objects (ex: 'annotationsForBodyDescription') as single source definitions
+    getterByKey(key: string): string {
+        // This is the downside, we need to list all our annotation getters here
+        const validGetterKeys = [
+            'testCase',
+            'prerequisites',
+            'steps',
+            'category',
+            'priority',
+            'stream',
+            'status',
+            'testRun',
+            'deviceModel',
+            'comment',
+        ] as const;
+
+        type StringGetterKey = (typeof validGetterKeys)[number];
+
+        const isValidKey = (k: string): k is StringGetterKey =>
+            validGetterKeys.includes(k as StringGetterKey);
+
+        if (!isValidKey(key)) {
+            throw new Error(
+                `The key '${key}' does not have corresponding getter on TestReportProvider class.`,
+            );
+        }
+
+        return this[key];
+    }
path: packages/suite-desktop-core/e2e/support/annotations.ts
user: login: coderabbitai[bot]
body: @HajekOndrej Thank you for confirming! The proposed refactoring will indeed make the code more type-safe and maintainable as new getters are added to the `TestReportProvider` class. The record-based approach eliminates the risk of runtime errors from using string indexing on `this` and provides better IDE support.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%20%F0%9F%9B%A0%EF%B8%8F%20Boost%20your%20code%20safety%20by%20replacing%20string-indexed%20getters%20with%20a%20typed%20map!%20Avoid%20runtime%20errors%20and%20scale%20effortlessly%20as%20keys%20grow.%20Try%20%40coderabbitai%20for%20smarter%20refactoring!%20%F0%9F%9A%80&url=https%3A%2F%2Fgithub.com%2Ftrezor%2Ftrezor-suite%2Fpull%2F18505%23discussion_r2056922475)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@HajekOndrej Thank you for confirming! The proposed refactoring will indeed make the code more type-safe and maintainable as new getters are added to the `TestReportProvider` class. The record-based approach eliminates the risk of runtime errors from using string indexing on `this` and provides better IDE support.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%20%F0%9F%9B%A0%EF%B8%8F%20Boost%20your%20code%20safety%20by%20replacing%20string-indexed%20getters%20with%20a%20typed%20map!%20Avoid%20runtime%20errors%20and%20scale%20effortlessly%20as%20keys%20grow.%20Try%20%40coderabbitai%20for%20smarter%20refactoring!%20%F0%9F%9A%80&url=https%3A%2F%2Fgithub.com%2Ftrezor%2Ftrezor-suite%2Fpull%2F18505%23discussion_r2056922475)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-24T13:40:46Z
author_association: NONE
diff_hunk: @@ -0,0 +1,280 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    ProjectQueryResponseUser,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
+
+    async createProjectField(
+        projectId: string,
+        fieldName: string,
+        fieldType: string,
+        options?: { value: string; color: string }[],
+    ): Promise<string> {
+        const isSelectField = fieldType === 'SINGLE_SELECT';
+        let optionsString = '';
+        this.logger.log(
+            `Creating ${fieldType} field "${fieldName}" in project ${projectId}${
+                isSelectField ? ` with ${options!.length} options` : ''
+            }...`,
+        );
+
+        if (isSelectField && options) {
+            optionsString = options
+                .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+                .join(',\n');
+        }
+
+        const mutation = `
+      mutation {
+        createProjectV2Field(
+        input: {
+          projectId: "${projectId}"
+          name: "${fieldName}"
+          dataType: ${fieldType}
+          ${isSelectField ? `singleSelectOptions: [${optionsString}]` : ''}
+        }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+        } 
+      }`;
+
+        const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
+        const fieldId = response.createProjectV2Field.projectV2Field.id;
+        this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
+
+        return fieldId;
+    }
+
+    async getProjectFromOrganization(
+        organization: string,
+        projectName: string,
+    ): Promise<Project[]> {
+        this.logger.log(`Fetching projects for organization: ${organization}`);
+
+        const query = `
+            query {
+              organization(login: "${organization}") {
+                projectsV2(first: 10, query: "${projectName}") {
+                  nodes {
+                    id
+                    title
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponse>(query);
+        this.logger.log('Successfully retrieved organization projects');
+
+        return response.organization.projectsV2.nodes;
+    }
+
+    async getProjectFromUser(ownerLogin: string, projectName: string): Promise<Project[]> {
+        this.logger.log(`Fetching projects for user: ${ownerLogin}`);
+
+        const query = `
+            query {
+              user(login: "${ownerLogin}") {
+                projectsV2(first: 10, query: "${projectName}") {
+                  nodes {
+                    id
+                    title
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponseUser>(query);
+        this.logger.log('Successfully retrieved user projects');
+
+        return response.user.projectsV2.nodes;
+    }
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    async getProjectFields(projectId: string): Promise<ProjectField[]> {
+        this.logger.log(`Fetching fields for project ${projectId}...`);
+
+        const response = await this.octokit.graphql<ProjectFieldsResponse>(`
+          query {
+            node(id: "${projectId}") {
+              ... on ProjectV2 {
+                fields(first: 20) {
+                  nodes {
+                    ... on ProjectV2FieldCommon {
+                      id
+                      name
+                      dataType
+                    }
+                    ... on ProjectV2SingleSelectField {
+                      id
+                      name
+                      dataType
+                      options {
+                        id
+                        name
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+      `);
+
+        this.logger.log(`Successfully retrieved fields for project ${projectId}`);
+
+        return response.node.fields.nodes;
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async updateFieldOptions(
+        fieldId: string,
+        options: { value: string; color: string }[],
+    ): Promise<void> {
+        this.logger.log(`Updating all options for field ${fieldId}`);
+
+        const optionsString = options
+            .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+            .join(',\n');
+
+        const mutation = `
+        mutation {
+          updateProjectV2Field(
+          input: {
+            fieldId: "${fieldId}"
+            singleSelectOptions: [
+              ${optionsString}
+            ]
+          }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+          } 
+        }
+      `;
+
+        await this.octokit.graphql(mutation);
+        this.logger.log(`Successfully replaced all options for field ${fieldId}`);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: Vere-Grey
body: Adopted.
created_at: 2025-04-24T19:14:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,280 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    ProjectQueryResponseUser,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
+
+    async createProjectField(
+        projectId: string,
+        fieldName: string,
+        fieldType: string,
+        options?: { value: string; color: string }[],
+    ): Promise<string> {
+        const isSelectField = fieldType === 'SINGLE_SELECT';
+        let optionsString = '';
+        this.logger.log(
+            `Creating ${fieldType} field "${fieldName}" in project ${projectId}${
+                isSelectField ? ` with ${options!.length} options` : ''
+            }...`,
+        );
+
+        if (isSelectField && options) {
+            optionsString = options
+                .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+                .join(',\n');
+        }
+
+        const mutation = `
+      mutation {
+        createProjectV2Field(
+        input: {
+          projectId: "${projectId}"
+          name: "${fieldName}"
+          dataType: ${fieldType}
+          ${isSelectField ? `singleSelectOptions: [${optionsString}]` : ''}
+        }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+        } 
+      }`;
+
+        const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
+        const fieldId = response.createProjectV2Field.projectV2Field.id;
+        this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
+
+        return fieldId;
+    }
+
+    async getProjectFromOrganization(
+        organization: string,
+        projectName: string,
+    ): Promise<Project[]> {
+        this.logger.log(`Fetching projects for organization: ${organization}`);
+
+        const query = `
+            query {
+              organization(login: "${organization}") {
+                projectsV2(first: 10, query: "${projectName}") {
+                  nodes {
+                    id
+                    title
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponse>(query);
+        this.logger.log('Successfully retrieved organization projects');
+
+        return response.organization.projectsV2.nodes;
+    }
+
+    async getProjectFromUser(ownerLogin: string, projectName: string): Promise<Project[]> {
+        this.logger.log(`Fetching projects for user: ${ownerLogin}`);
+
+        const query = `
+            query {
+              user(login: "${ownerLogin}") {
+                projectsV2(first: 10, query: "${projectName}") {
+                  nodes {
+                    id
+                    title
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponseUser>(query);
+        this.logger.log('Successfully retrieved user projects');
+
+        return response.user.projectsV2.nodes;
+    }
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    async getProjectFields(projectId: string): Promise<ProjectField[]> {
+        this.logger.log(`Fetching fields for project ${projectId}...`);
+
+        const response = await this.octokit.graphql<ProjectFieldsResponse>(`
+          query {
+            node(id: "${projectId}") {
+              ... on ProjectV2 {
+                fields(first: 20) {
+                  nodes {
+                    ... on ProjectV2FieldCommon {
+                      id
+                      name
+                      dataType
+                    }
+                    ... on ProjectV2SingleSelectField {
+                      id
+                      name
+                      dataType
+                      options {
+                        id
+                        name
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+      `);
+
+        this.logger.log(`Successfully retrieved fields for project ${projectId}`);
+
+        return response.node.fields.nodes;
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async updateFieldOptions(
+        fieldId: string,
+        options: { value: string; color: string }[],
+    ): Promise<void> {
+        this.logger.log(`Updating all options for field ${fieldId}`);
+
+        const optionsString = options
+            .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+            .join(',\n');
+
+        const mutation = `
+        mutation {
+          updateProjectV2Field(
+          input: {
+            fieldId: "${fieldId}"
+            singleSelectOptions: [
+              ${optionsString}
+            ]
+          }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+          } 
+        }
+      `;
+
+        await this.octokit.graphql(mutation);
+        this.logger.log(`Successfully replaced all options for field ${fieldId}`);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
+              body: "${body.replace(/"/g, '\\"')}"
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: Vere-Grey
body: Adopted.
created_at: 2025-04-24T19:14:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Intentional. This is the first implementation step. Follow up will introduce workflow part which will have these variables in env vars. So this is outscoped to the next PR
created_at: 2025-04-24T19:16:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: coderabbitai[bot]
body: @Vere-Grey I understand! Your phased implementation approach makes sense - implement the core GitHub reporter functionality first, then add environment variable configuration in the follow-up PR with the workflow integration. Thanks for clarifying your strategy.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey I understand! Your phased implementation approach makes sense - implement the core GitHub reporter functionality first, then add environment variable configuration in the follow-up PR with the workflow integration. Thanks for clarifying your strategy.

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-24T19:16:38Z
author_association: NONE
diff_hunk: @@ -0,0 +1,258 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: Vere-Grey
body: The error handling is done on caller level. Atm it wraps whole InitilizeProject. I could add more granular try catch. But I would definitelly keep it on caller level and not in method itself.  What do you think @HajekOndrej ? This current state:

```
try {
            const existingProject = await this.findExistingProject();

            if (existingProject) {
                this._projectId = existingProject.id;
                this.log(
                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
                );

                return;
            }
            await this.createProject(annotationsForProjectFields);

            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
            const createdProject = await this.findExistingProject();
            if (createdProject) {
                this._projectId = createdProject.id;
                this.log(`Using created project: ${createdProject.title} (${createdProject.id})`);

                return;
            } else {
                throw new Error('Failed to find the created project');
            }
        } catch (error) {
            this.logError('Project initialization failed.');
            throw error;
        }
```
created_at: 2025-04-24T19:34:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,258 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-04-24T19:34:25Z
author_association: NONE
diff_hunk: @@ -0,0 +1,258 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: Vere-Grey
body: Good idea. I will improve naming that will reflect that those are just request and split it to two parts
- project 
- issues
created_at: 2025-04-24T19:35:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: I believe _varName should be used for properties that have getters and should never be access directly.

Because of the 'dynamic exports' of octokit. We are initilizing the octokit object in onBegin and as such if onBegin fails, the property will be null.
For that reason we have a getter. So that we dont have to check the null value in every use
```
    private get octokit(): Octokit {
        if (!this._octokit) {
            throw new Error(
                'Octokit instance is not initialized. Ensure onBegin() is called first.',
            );
        }

        return this._octokit;
    }
```

The whole dynamic export is new thing for me. And if something is not my cup of tea - its imports. I will try to read more about it to see if there is better approach.
created_at: 2025-04-24T19:39:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Good catch! :+1: 
I haven't expected posibility of onTestEnd being faster or onBegin failing in a bad way and not setting the property. I will redo this. 
created_at: 2025-04-24T19:44:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: It is a quite powerful tool from our utils, Recommended by Varmuza in one my previous PRs.
Based on config it can do many things. In our case it is configured as a retry mechanism of our calls.
I just notice that I added those for the onTestEnd but it is completlly missing for the onBegin. I will add those there.

onBegin havent failed for me once during my testing. onTestEnd did fail regularly. But that is not garanteed that the onBegin will succeed. So I will add it there too.
created_at: 2025-04-24T19:47:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
+                        this.log(`Creating GitHub draft issue for test "${test.title}"...`);
+
+                        return this.graphQLClient.createDraftIssueInProject(
+                            this.projectId,
+                            report.testCase,
+                            report.bodyDescription,
+                        );
+                    }, RETRY_CONF);
+                    this.log(`[${issueNodeId}] Successfully created issue "${test.title}"`);
+
+                    const fields = await scheduleAction(() => this.getProjectFields(), RETRY_CONF);
+
+                    for (const { name, value } of report.projectValues) {
+                        const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(
+                            fields,
+                            name,
+                            value,
+                        );
+                        await scheduleAction(() => {
+                            this.log(`[${issueNodeId}] Updating field ${name}:"${value}"...`);
+
+                            return this.graphQLClient.setItemValue(
+                                this.projectId,
+                                issueNodeId,
+                                fieldId,
+                                valueOrOptionId,
+                            );
+                        }, RETRY_CONF);
+                        this.log(`[${issueNodeId}] Successfully updated field ${name}:"${value}"`);
+                    }
+
+                    this.log(
+                        `[${issueNodeId}] Successfully recorded test result for "${test.title}"`,
+                    );
+                } catch (error) {
+                    this.logError(`Failed to process test end for "${test.title}":`, error);
+                    // Non-Critical error, no need to rethrow
+                }
+            })(),
+        );
+    }
+
+    // Finalizes reporting when all tests are complete, waits for pending operations to finish
+    // eslint-disable-next-line require-await
+    async onEnd() {
+        this.log('All tests completed, waiting for pending operations...');
+
+        if (this.pendingOperations.length > 0) {
+            this.log(`Waiting for ${this.pendingOperations.length} pending operations to complete`);
+
+            return Promise.allSettled(this.pendingOperations)
+                .then(results => {
+                    const failed = results.filter(r => r.status === 'rejected').length;
+                    if (failed > 0) {
+                        this.logError(`${failed} operations failed`);
+                    } else {
+                        this.log('All operations completed successfully');
+                    }
+                })
+                .finally(() => {
+                    this.log('GitHub reporter finished');
+                });
+        } else {
+            this.log('No pending operations, GitHub reporter finished');
+
+            return Promise.resolve();
+        }
+    }
+
+    private async initializeProject(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.log(`Using created project: ${createdProject.title} (${createdProject.id})`);
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
+        try {
+            const projects = await this.graphQLClient.getProjectFromOrganization(
+                ORGANIZATION,
+                PROJECT_NAME,
+            );
+
+            const matchingProject = projects.find((project: any) => project.title === PROJECT_NAME);
+
+            if (matchingProject) {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Love it. will do :)
created_at: 2025-04-24T19:47:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
+                        this.log(`Creating GitHub draft issue for test "${test.title}"...`);
+
+                        return this.graphQLClient.createDraftIssueInProject(
+                            this.projectId,
+                            report.testCase,
+                            report.bodyDescription,
+                        );
+                    }, RETRY_CONF);
+                    this.log(`[${issueNodeId}] Successfully created issue "${test.title}"`);
+
+                    const fields = await scheduleAction(() => this.getProjectFields(), RETRY_CONF);
+
+                    for (const { name, value } of report.projectValues) {
+                        const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(
+                            fields,
+                            name,
+                            value,
+                        );
+                        await scheduleAction(() => {
+                            this.log(`[${issueNodeId}] Updating field ${name}:"${value}"...`);
+
+                            return this.graphQLClient.setItemValue(
+                                this.projectId,
+                                issueNodeId,
+                                fieldId,
+                                valueOrOptionId,
+                            );
+                        }, RETRY_CONF);
+                        this.log(`[${issueNodeId}] Successfully updated field ${name}:"${value}"`);
+                    }
+
+                    this.log(
+                        `[${issueNodeId}] Successfully recorded test result for "${test.title}"`,
+                    );
+                } catch (error) {
+                    this.logError(`Failed to process test end for "${test.title}":`, error);
+                    // Non-Critical error, no need to rethrow
+                }
+            })(),
+        );
+    }
+
+    // Finalizes reporting when all tests are complete, waits for pending operations to finish
+    // eslint-disable-next-line require-await
+    async onEnd() {
+        this.log('All tests completed, waiting for pending operations...');
+
+        if (this.pendingOperations.length > 0) {
+            this.log(`Waiting for ${this.pendingOperations.length} pending operations to complete`);
+
+            return Promise.allSettled(this.pendingOperations)
+                .then(results => {
+                    const failed = results.filter(r => r.status === 'rejected').length;
+                    if (failed > 0) {
+                        this.logError(`${failed} operations failed`);
+                    } else {
+                        this.log('All operations completed successfully');
+                    }
+                })
+                .finally(() => {
+                    this.log('GitHub reporter finished');
+                });
+        } else {
+            this.log('No pending operations, GitHub reporter finished');
+
+            return Promise.resolve();
+        }
+    }
+
+    private async initializeProject(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.log(`Using created project: ${createdProject.title} (${createdProject.id})`);
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Good idea! I will split it 
created_at: 2025-04-24T19:48:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Any idea how we could make it more clear? We are mentioning retries in docu. I will add there mention of this method. But still it would be nice if that is obvious just from code itself. :thinking: 
created_at: 2025-04-24T19:52:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Oh, it is already mentioned there. But still. I agree we should make it maybe more obvious from the code.
created_at: 2025-04-24T19:53:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Octokit uses conditiona exports. That is something I am not familiar with. What I understand that without  changin tsconfig. I cannot do the normal dynamic import. Please correct me if I am wrong. But I also need a type for class property. I will improve it by importing just the type at the start `import type { Octokit } from '@octokit/rest';`
https://www.npmjs.com/package/@octokit/rest

```
[!IMPORTANT] As we use [conditional exports](https://nodejs.org/api/packages.html#conditional-exports), you will need to adapt your tsconfig.json by setting "moduleResolution": "node16", "module": "node16".

See the TypeScript docs on [package.json "exports"](https://www.typescriptlang.org/docs/handbook/modules/reference.html#packagejson-exports).
See this [helpful guide on transitioning to ESM](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) from [@sindresorhus](https://github.com/sindresorhus)
```


created_at: 2025-04-24T20:58:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: I will try to educate myself more tomorrow when I have a fresher mind.
created_at: 2025-04-24T20:59:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: coderabbitai[bot]
body: @Vere-Grey No worries about revisiting this tomorrow! Your suggestion about using `import type { Octokit } from '@octokit/rest';` is spot on - it's the right approach for this situation.

This pattern keeps the TypeScript type definitions for compile-time checking while avoiding duplicate imports:

```typescript
// For TypeScript type checking only (no runtime code)
import type { Octokit } from '@octokit/rest';

class GitHubReporter implements Reporter, LoggingFunctions {
    private _octokit: Octokit | null = null;
    
    // Rest of your implementation...
    
    async onBegin() {
        const initPromise = (async () => {
            try {
                const OctokitModule = await import('@octokit/rest');
                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
                // ...
            }
        })();
        // ...
    }
}
```

This cleanly separates the type information from the runtime implementation, which is ideal when dealing with conditional exports.

---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey No worries about revisiting this tomorrow! Your suggestion about using `import type { Octokit } from '@octokit/rest';` is spot on - it's the right approach for this situation.

This pattern keeps the TypeScript type definitions for compile-time checking while avoiding duplicate imports:

```typescript
// For TypeScript type checking only (no runtime code)
import type { Octokit } from '@octokit/rest';

class GitHubReporter implements Reporter, LoggingFunctions {
    private _octokit: Octokit | null = null;
    
    // Rest of your implementation...
    
    async onBegin() {
        const initPromise = (async () => {
            try {
                const OctokitModule = await import('@octokit/rest');
                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
                // ...
            }
        })();
        // ...
    }
}
```

This cleanly separates the type information from the runtime implementation, which is ideal when dealing with conditional exports.

---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-24T20:59:47Z
author_association: NONE
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: coderabbitai[bot]
body: @Vere-Grey You're absolutely right about using `import type`. This is the perfect solution for your case:

```diff
-import { Octokit } from '@octokit/rest';
+import type { Octokit } from '@octokit/rest';
```

This approach gives you TypeScript type checking for your class property while avoiding duplicated module loading. The dynamic import in `onBegin()` will still handle the actual runtime module loading.

Your solution is more targeted and less invasive than changing the `tsconfig.json` settings, which would potentially affect the entire project. Nice catch on understanding the conditional exports issue!

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey You're absolutely right about using `import type`. This is the perfect solution for your case:

```diff
-import { Octokit } from '@octokit/rest';
+import type { Octokit } from '@octokit/rest';
```

This approach gives you TypeScript type checking for your class property while avoiding duplicated module loading. The dynamic import in `onBegin()` will still handle the actual runtime module loading.

Your solution is more targeted and less invasive than changing the `tsconfig.json` settings, which would potentially affect the entire project. Nice catch on understanding the conditional exports issue!

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-24T21:02:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Import Octokit as a type-only dependency to prevent runtime ESM resolution errors**

`@octokit/rest` uses conditional exports; static loading here may fail under the default TypeScript/Node resolution settings and also duplicates the dynamic load performed in `GitHubReporter`. You only need the type, so switch to a type-only import to remove the unnecessary runtime require.

```diff
-import { Octokit } from '@octokit/rest';
+import type { Octokit } from '@octokit/rest';
```

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:25:55Z
author_association: NONE
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
+              body: "${body.replace(/"/g, '\\"')}"
+            }
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Inline-string GraphQL mutations are fragile & injection-prone – use variables instead**

`title` and `body` are interpolated directly after a simple `replace(/"/g, '\\"')`, which

1. still breaks on line breaks/back-slashes  
2. opens the door to accidental GraphQL injection if a test title contains `"} } mutation {`…  
3. produces unreadable mutations.

Leverage GraphQL variables instead of string interpolation:

```diff
-const mutation = `
-  mutation {
-    addProjectV2DraftIssue(
-      input: {
-        projectId: "${projectId}"
-        title: "${title.replace(/"/g, '\\"')}"
-        body: "${body.replace(/"/g, '\\"')}"
-      }
-    ) {
-      projectItem { id }
-    }
-  }
-`;
+const mutation = `
+  mutation addDraft($projectId:ID!, $title:String!, $body:String!) {
+    addProjectV2DraftIssue(
+      input: { projectId: $projectId, title: $title, body: $body, assigneeIds: [] }
+    ) { projectItem { id } }
+  }
+`;
+const response = await this.octokit.graphql<AddDraftIssueResponse>(mutation, {
+  projectId,
+  title,
+  body,
+});
```

This eliminates escaping headaches and guards against injection.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        // Replace the inline-escaped mutation with a parameterized GraphQL operation
        const mutation = `
          mutation addDraft($projectId: ID!, $title: String!, $body: String!) {
            addProjectV2DraftIssue(
              input: { projectId: $projectId, title: $title, body: $body, assigneeIds: [] }
            ) {
              projectItem { id }
            }
          }
        `;
        const response = await this.octokit.graphql<AddDraftIssueResponse>(mutation, {
          projectId,
          title,
          body,
        });
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:25:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,279 @@
+import type { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubProject } from './gitHubProject';
+import { IssueRequests } from './issueRequests';
+import { LoggingFunctions, ProjectField } from './types';
+
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+enum InitializationState {
+    NOT_STARTED = 'NOT_STARTED',
+    IN_PROGRESS = 'IN_PROGRESS',
+    COMPLETED = 'COMPLETED',
+    FAILED = 'FAILED',
+}
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _issueRequests: IssueRequests | null = null;
+    private _gitHubProject: GitHubProject | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initState: InitializationState = InitializationState.NOT_STARTED;
+
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get issueRequests(): IssueRequests {
+        if (!this._issueRequests) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._issueRequests;
+    }
+
+    private get gitHubProject(): GitHubProject {
+        if (!this._gitHubProject) {
+            throw new Error('GitHub project is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._gitHubProject;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        this.initState = InitializationState.IN_PROGRESS;
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._issueRequests = new IssueRequests(this.octokit);
+                this._gitHubProject = new GitHubProject(this.octokit, this);
+                this.initState = InitializationState.COMPLETED;
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.initState = InitializationState.FAILED;
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.gitHubProject.init();
+        })();
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Race-condition: `initState` marked COMPLETED before the project is ready**

`initState` flips to `COMPLETED` **before** `this.gitHubProject.init()` resolves.  
If a fast `onTestEnd` fires in that window, `waitForOnBeginInit()` returns early, the reporter
tries to use `this.gitHubProject.id`, and immediately throws “Project ID is not set”.

```diff
-                this.initState = InitializationState.COMPLETED;
-                this.log('GitHub client initialized successfully');
-            } catch (error) {
-                ...
-            }
-
-            await this.gitHubProject.init();
+                this.log('GitHub client initialized successfully');
+                await this.gitHubProject.init();
+                this.initState = InitializationState.COMPLETED;
+            } catch (error) {
+                ...
+            }
```

Move the state change **after** successful project initialisation (and keep it inside the `try`), or make `waitForOnBeginInit()` additionally await `initializationPromise`.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
        this.initState = InitializationState.IN_PROGRESS;
        const initPromise = (async () => {
            try {
                const OctokitModule = await import('@octokit/rest');
                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
                this._issueRequests = new IssueRequests(this.octokit);
                this._gitHubProject = new GitHubProject(this.octokit, this);
                this.log('GitHub client initialized successfully');
                await this.gitHubProject.init();
                this.initState = InitializationState.COMPLETED;
            } catch (error) {
                this.initState = InitializationState.FAILED;
                this.logError('Failed to initialize GitHub reporter.');
                throw error; // Critical error, rethrow to stop execution
            }
        })();
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->

✅ Addressed in commit 2ea5015
created_at: 2025-04-25T06:25:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,165 @@
+import { Octokit } from '@octokit/rest';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { ProjectRequests } from './projectRequests';
+import { LoggingFunctions } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 37'; // TODO: Get this from CI, probably build name
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+export class GitHubProject {
+    private _projectId: string | undefined;
+    private graphQLClient: ProjectRequests;
+
+    constructor(
+        octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {
+        this.graphQLClient = new ProjectRequests(octokit, logger);
+    }
+
+    get id(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    async init(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.logger.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.logger.log(
+                    `Using created project: ${createdProject.title} (${createdProject.id})`,
+                );
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logger.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
+        try {
+            const projects = await scheduleAction(
+                () => this.graphQLClient.getProjectFromOrganization(ORGANIZATION, PROJECT_NAME),
+                RETRY_CONF,
+            );
+
+            const matchingProject = projects.find((project: any) => project.title === PROJECT_NAME);
+
+            if (!matchingProject) {
+                return null;
+            }
+
+            const areThereDuplicates =
+                projects.filter((project: any) => project.title === PROJECT_NAME).length > 1;
+            if (areThereDuplicates) {
+                this.logger.log(
+                    `Warning: Multiple projects found with title "${PROJECT_NAME}". Using the first one with number ${matchingProject.number}.`,
+                );
+            }
+
path: packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Non-deterministic selection when multiple projects share the same name**

`findExistingProject` picks `projects.find(...)` (first element returned by GitHub),
but GraphQL doesn’t guarantee ordering. Two parallel CI jobs could therefore
attach to different boards.

Sort deterministically (e.g. by `createdAt` or `number`) before picking:

```typescript
const matchingProjects = projects
  .filter(p => p.title === PROJECT_NAME)
  .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());

const oldest = matchingProjects[0];
```

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:25:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,165 @@
+import { Octokit } from '@octokit/rest';
path: packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Import Octokit as type-only – runtime value is unused**

`Octokit` is referenced only for typing in the constructor; the runtime module is never used.
Avoid loading the package twice and sidestep conditional-export pitfalls:

```diff
-import { Octokit } from '@octokit/rest';
+import type { Octokit } from '@octokit/rest';
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
import type { Octokit } from '@octokit/rest';
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:25:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,190 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+} from './types';
+
+export async function getProjectFields(
+    octokit: Octokit,
+    projectId: string,
+): Promise<ProjectField[]> {
+    const query = `
+        query {
+          node(id: "${projectId}") {
+            ... on ProjectV2 {
+              fields(first: 20) {
+                nodes {
+                  ... on ProjectV2FieldCommon {
+                    id
+                    name
+                    dataType
+                  }
+                  ... on ProjectV2SingleSelectField {
+                    id
+                    name
+                    dataType
+                    options {
+                      id
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    `;
+
+    const response = await octokit.graphql<ProjectFieldsResponse>(query);
+
+    return response.node.fields.nodes;
+}
path: packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Consider adding error handling to the `getProjectFields` function**

The function fetches project fields via GraphQL but lacks error handling for API failures. GitHub API calls can fail due to rate limits, network issues, or permission problems.


```diff
export async function getProjectFields(
    octokit: Octokit,
    projectId: string,
): Promise<ProjectField[]> {
    const query = `
        query {
          node(id: "${projectId}") {
            ... on ProjectV2 {
              fields(first: 20) {
                nodes {
                  ... on ProjectV2FieldCommon {
                    id
                    name
                    dataType
                  }
                  ... on ProjectV2SingleSelectField {
                    id
                    name
                    dataType
                    options {
                      id
                      name
                    }
                  }
                }
              }
            }
          }
        }
    `;

-    const response = await octokit.graphql<ProjectFieldsResponse>(query);
-
-    return response.node.fields.nodes;
+    try {
+        const response = await octokit.graphql<ProjectFieldsResponse>(query);
+        return response.node.fields.nodes;
+    } catch (error) {
+        console.error(`Failed to fetch project fields: ${error.message}`);
+        throw new Error(`Failed to fetch fields for project ${projectId}: ${error.message}`);
+    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export async function getProjectFields(
    octokit: Octokit,
    projectId: string,
): Promise<ProjectField[]> {
    const query = `
        query {
          node(id: "${projectId}") {
            ... on ProjectV2 {
              fields(first: 20) {
                nodes {
                  ... on ProjectV2FieldCommon {
                    id
                    name
                    dataType
                  }
                  ... on ProjectV2SingleSelectField {
                    id
                    name
                    dataType
                    options {
                      id
                      name
                    }
                  }
                }
              }
            }
          }
        }
    `;

    try {
        const response = await octokit.graphql<ProjectFieldsResponse>(query);
        return response.node.fields.nodes;
    } catch (error: any) {
        console.error(`Failed to fetch project fields: ${error.message}`);
        throw new Error(`Failed to fetch fields for project ${projectId}: ${error.message}`);
    }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:25:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,190 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+} from './types';
+
+export async function getProjectFields(
+    octokit: Octokit,
+    projectId: string,
+): Promise<ProjectField[]> {
+    const query = `
+        query {
+          node(id: "${projectId}") {
+            ... on ProjectV2 {
+              fields(first: 20) {
+                nodes {
+                  ... on ProjectV2FieldCommon {
+                    id
+                    name
+                    dataType
+                  }
+                  ... on ProjectV2SingleSelectField {
+                    id
+                    name
+                    dataType
+                    options {
+                      id
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    `;
+
+    const response = await octokit.graphql<ProjectFieldsResponse>(query);
+
+    return response.node.fields.nodes;
+}
+
+export class ProjectRequests {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
+
+    async createProjectField(
+        projectId: string,
+        fieldName: string,
+        fieldType: string,
+        options?: { value: string; color: string }[],
+    ): Promise<string> {
+        const isSelectField = fieldType === 'SINGLE_SELECT';
+        let optionsString = '';
+        this.logger.log(
+            `Creating ${fieldType} field "${fieldName}" in project ${projectId}${
+                isSelectField ? ` with ${options!.length} options` : ''
+            }...`,
+        );
+
+        if (isSelectField && options) {
+            optionsString = options
+                .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+                .join(',\n');
+        }
+
+        const mutation = `
+      mutation {
+        createProjectV2Field(
+        input: {
+          projectId: "${projectId}"
+          name: "${fieldName}"
+          dataType: ${fieldType}
+          ${isSelectField ? `singleSelectOptions: [${optionsString}]` : ''}
+        }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+        } 
+      }`;
+
+        const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
+        const fieldId = response.createProjectV2Field.projectV2Field.id;
+        this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
+
+        return fieldId;
+    }
+
+    async getProjectFromOrganization(
+        organization: string,
+        projectName: string,
+    ): Promise<Project[]> {
+        this.logger.log(`Fetching projects for organization: ${organization}`);
+
+        const query = `
+            query {
+              organization(login: "${organization}") {
+                projectsV2(first: 30, query: "${projectName}", orderBy: {field: NUMBER, direction: ASC}) {
+                  nodes {
+                    id
+                    title
+                    number
+                  }
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<ProjectQueryResponse>(query);
+        this.logger.log('Successfully retrieved organization projects');
+
+        return response.organization.projectsV2.nodes;
+    }
+
+    async getProjectFields(projectId: string): Promise<ProjectField[]> {
+        this.logger.log(`Fetching fields for project ${projectId}...`);
+        const response = await getProjectFields(this.octokit, projectId);
+        this.logger.log(`Successfully retrieved fields for project ${projectId}`);
+
+        return response;
+    }
+
+    async updateFieldOptions(
+        fieldId: string,
+        options: { value: string; color: string }[],
+    ): Promise<void> {
+        this.logger.log(`Updating all options for field ${fieldId}`);
+
+        const optionsString = options
+            .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+            .join(',\n');
+
+        const mutation = `
+        mutation {
+          updateProjectV2Field(
+          input: {
+            fieldId: "${fieldId}"
+            singleSelectOptions: [
+              ${optionsString}
+            ]
+          }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+          } 
+        }
+      `;
+
+        await this.octokit.graphql(mutation);
+        this.logger.log(`Successfully replaced all options for field ${fieldId}`);
+    }
path: packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add error handling and input validation to `updateFieldOptions`**

The method should validate inputs and handle potential API errors.


```diff
async updateFieldOptions(
    fieldId: string,
    options: { value: string; color: string }[],
): Promise<void> {
+    if (!fieldId || !options || options.length === 0) {
+        throw new Error('Field ID and at least one option are required');
+    }
+    
    this.logger.log(`Updating all options for field ${fieldId}`);

    const optionsString = options
-        .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+        .map(opt => `{ name: "${opt.value.replace(/"/g, '\\"')}", description: "", color: ${opt.color} }`)
        .join(',\n');

    const mutation = `
    mutation {
      updateProjectV2Field(
      input: {
        fieldId: "${fieldId}"
        singleSelectOptions: [
          ${optionsString}
        ]
      }) {
        projectV2Field {
          ... on ProjectV2SingleSelectField {
            id
          }
        }
      } 
    }
  `;

-    await this.octokit.graphql(mutation);
-    this.logger.log(`Successfully replaced all options for field ${fieldId}`);
+    try {
+        await this.octokit.graphql(mutation);
+        this.logger.log(`Successfully replaced all options for field ${fieldId}`);
+    } catch (error) {
+        this.logger.error(`Failed to update field options: ${error.message}`);
+        throw new Error(`Failed to update options for field ${fieldId}: ${error.message}`);
+    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
async updateFieldOptions(
    fieldId: string,
    options: { value: string; color: string }[],
): Promise<void> {
    if (!fieldId || !options || options.length === 0) {
        throw new Error('Field ID and at least one option are required');
    }
    
    this.logger.log(`Updating all options for field ${fieldId}`);

    const optionsString = options
        .map(opt => `{ name: "${opt.value.replace(/"/g, '\\"')}", description: "", color: ${opt.color} }`)
        .join(',\n');

    const mutation = `
    mutation {
      updateProjectV2Field(
      input: {
        fieldId: "${fieldId}"
        singleSelectOptions: [
          ${optionsString}
        ]
      }) {
        projectV2Field {
          ... on ProjectV2SingleSelectField {
            id
          }
        }
      } 
    }
  `;

    try {
        await this.octokit.graphql(mutation);
        this.logger.log(`Successfully replaced all options for field ${fieldId}`);
    } catch (error) {
        this.logger.error(`Failed to update field options: ${error.message}`);
        throw new Error(`Failed to update options for field ${fieldId}: ${error.message}`);
    }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-25T06:25:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,190 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+} from './types';
+
+export async function getProjectFields(
+    octokit: Octokit,
+    projectId: string,
+): Promise<ProjectField[]> {
+    const query = `
+        query {
+          node(id: "${projectId}") {
+            ... on ProjectV2 {
+              fields(first: 20) {
+                nodes {
+                  ... on ProjectV2FieldCommon {
+                    id
+                    name
+                    dataType
+                  }
+                  ... on ProjectV2SingleSelectField {
+                    id
+                    name
+                    dataType
+                    options {
+                      id
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    `;
+
+    const response = await octokit.graphql<ProjectFieldsResponse>(query);
+
+    return response.node.fields.nodes;
+}
+
+export class ProjectRequests {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
+
+    async createProjectField(
+        projectId: string,
+        fieldName: string,
+        fieldType: string,
+        options?: { value: string; color: string }[],
+    ): Promise<string> {
+        const isSelectField = fieldType === 'SINGLE_SELECT';
+        let optionsString = '';
+        this.logger.log(
+            `Creating ${fieldType} field "${fieldName}" in project ${projectId}${
+                isSelectField ? ` with ${options!.length} options` : ''
+            }...`,
+        );
+
+        if (isSelectField && options) {
+            optionsString = options
+                .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+                .join(',\n');
+        }
+
+        const mutation = `
+      mutation {
+        createProjectV2Field(
+        input: {
+          projectId: "${projectId}"
+          name: "${fieldName}"
+          dataType: ${fieldType}
+          ${isSelectField ? `singleSelectOptions: [${optionsString}]` : ''}
+        }) {
+            projectV2Field {
+              ... on ProjectV2SingleSelectField {
+                id
+              }
+            }
+        } 
+      }`;
+
+        const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
+        const fieldId = response.createProjectV2Field.projectV2Field.id;
+        this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
+
+        return fieldId;
+    }
path: packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add input sanitization and error handling to `createProjectField`**

The method doesn't sanitize inputs for the GraphQL query which could lead to potential injection issues. Also, error handling should be added.


```diff
async createProjectField(
    projectId: string,
    fieldName: string,
    fieldType: string,
    options?: { value: string; color: string }[],
): Promise<string> {
+    if (!projectId || !fieldName || !fieldType) {
+        throw new Error('Project ID, field name, and field type are required');
+    }
+    
+    // Sanitize inputs for GraphQL
+    const sanitizedFieldName = fieldName.replace(/"/g, '\\"');
+    
    const isSelectField = fieldType === 'SINGLE_SELECT';
    let optionsString = '';
    this.logger.log(
        `Creating ${fieldType} field "${fieldName}" in project ${projectId}${
            isSelectField ? ` with ${options!.length} options` : ''
        }...`,
    );

    if (isSelectField && options) {
        optionsString = options
-            .map(opt => `{ name: "${opt.value}", description: "", color: ${opt.color} }`)
+            .map(opt => `{ name: "${opt.value.replace(/"/g, '\\"')}", description: "", color: ${opt.color} }`)
            .join(',\n');
    }

    const mutation = `
  mutation {
    createProjectV2Field(
    input: {
      projectId: "${projectId}"
-      name: "${fieldName}"
+      name: "${sanitizedFieldName}"
      dataType: ${fieldType}
      ${isSelectField ? `singleSelectOptions: [${optionsString}]` : ''}
    }) {
        projectV2Field {
          ... on ProjectV2SingleSelectField {
            id
          }
        }
    } 
  }`;

-    const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
-    const fieldId = response.createProjectV2Field.projectV2Field.id;
-    this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
-
-    return fieldId;
+    try {
+        const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
+        const fieldId = response.createProjectV2Field.projectV2Field.id;
+        this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
+        return fieldId;
+    } catch (error) {
+        this.logger.error(`Failed to create field: ${error.message}`);
+        throw new Error(`Failed to create field "${fieldName}": ${error.message}`);
+    }
}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    async createProjectField(
        projectId: string,
        fieldName: string,
        fieldType: string,
        options?: { value: string; color: string }[],
    ): Promise<string> {
        if (!projectId || !fieldName || !fieldType) {
            throw new Error('Project ID, field name, and field type are required');
        }
        
        // Sanitize inputs for GraphQL
        const sanitizedFieldName = fieldName.replace(/"/g, '\\"');

        const isSelectField = fieldType === 'SINGLE_SELECT';
        let optionsString = '';
        this.logger.log(
            `Creating ${fieldType} field "${fieldName}" in project ${projectId}${
                isSelectField ? ` with ${options!.length} options` : ''
            }...`,
        );

        if (isSelectField && options) {
            optionsString = options
                .map(opt => `{ name: "${opt.value.replace(/"/g, '\\"')}", description: "", color: ${opt.color} }`)
                .join(',\n');
        }

        const mutation = `
      mutation {
        createProjectV2Field(
        input: {
          projectId: "${projectId}"
          name: "${sanitizedFieldName}"
          dataType: ${fieldType}
          ${isSelectField ? `singleSelectOptions: [${optionsString}]` : ''}
        }) {
            projectV2Field {
              ... on ProjectV2SingleSelectField {
                id
              }
            }
        } 
      }`;

        try {
            const response = await this.octokit.graphql<CreateFieldResponse>(mutation);
            const fieldId = response.createProjectV2Field.projectV2Field.id;
            this.logger.log(`Created ${fieldType} field "${fieldName}" with ID: ${fieldId}`);
            return fieldId;
        } catch (error) {
            this.logger.error(`Failed to create field: ${error.message}`);
            throw new Error(`Failed to create field "${fieldName}": ${error.message}`);
        }
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-25T06:25:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,190 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+} from './types';
+
+export async function getProjectFields(
+    octokit: Octokit,
+    projectId: string,
+): Promise<ProjectField[]> {
+    const query = `
+        query {
+          node(id: "${projectId}") {
+            ... on ProjectV2 {
+              fields(first: 20) {
+                nodes {
+                  ... on ProjectV2FieldCommon {
+                    id
+                    name
+                    dataType
+                  }
+                  ... on ProjectV2SingleSelectField {
+                    id
+                    name
+                    dataType
+                    options {
+                      id
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    `;
+
+    const response = await octokit.graphql<ProjectFieldsResponse>(query);
+
+    return response.node.fields.nodes;
+}
+
+export class ProjectRequests {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
path: packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add validation and check if project already exists before creation**

The `createProject` method doesn't validate inputs or check if a project with the same name already exists, which could lead to duplicate projects.


```diff
async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+    if (!ownerId || !projectName) {
+        throw new Error('Owner ID and project name are required');
+    }
+    
+    // Check if project already exists
+    const existingProjects = await this.getProjectFromOrganization(
+        ownerId.startsWith('O_') ? ownerId.substring(2) : ownerId,
+        projectName
+    );
+    
+    const existingProject = existingProjects.find(p => p.title === projectName);
+    if (existingProject) {
+        this.logger.log(`Project "${projectName}" already exists with ID: ${existingProject.id}`);
+        return existingProject.id;
+    }
+    
    this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);

    const mutation = `
        mutation {
            createProjectV2(
            input: {
                ownerId: "${ownerId}"
                title: "${projectName}"
                teamId: "${teamId}"
            }) {
                projectV2 {
                    id
                }
            }
        }
    `;

-    const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+    try {
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+        return projectId;
+    } catch (error) {
+        this.logger.error(`Failed to create project: ${error.message}`);
+        throw new Error(`Failed to create project "${projectName}": ${error.message}`);
+    }
-    const projectId = response.createProjectV2.projectV2.id;
-
-    this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
-
-    return projectId;
}
```


> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:25:56Z
author_association: NONE
diff_hunk: @@ -0,0 +1,258 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: Vere-Grey
body: I have split of the project methods. Kept there the setup methods and issue methods. Let me know if you see more opportunities there to split :)
created_at: 2025-04-25T06:29:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: I reworked this. Let me know if you see any forgotten holes there :)
created_at: 2025-04-25T06:30:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: github-advanced-security[bot]
body: ## Incomplete string escaping or encoding

This does not escape backslash characters in the input.

[Show more details](https://github.com/trezor/trezor-suite/security/code-scanning/182)
created_at: 2025-04-25T06:31:00Z
author_association: NONE
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
+              body: "${body.replace(/"/g, '\\"')}"
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: github-advanced-security[bot]
body: ## Incomplete string escaping or encoding

This does not escape backslash characters in the input.

[Show more details](https://github.com/trezor/trezor-suite/security/code-scanning/183)
created_at: 2025-04-25T06:31:00Z
author_association: NONE
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
+                        this.log(`Creating GitHub draft issue for test "${test.title}"...`);
+
+                        return this.graphQLClient.createDraftIssueInProject(
+                            this.projectId,
+                            report.testCase,
+                            report.bodyDescription,
+                        );
+                    }, RETRY_CONF);
+                    this.log(`[${issueNodeId}] Successfully created issue "${test.title}"`);
+
+                    const fields = await scheduleAction(() => this.getProjectFields(), RETRY_CONF);
+
+                    for (const { name, value } of report.projectValues) {
+                        const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(
+                            fields,
+                            name,
+                            value,
+                        );
+                        await scheduleAction(() => {
+                            this.log(`[${issueNodeId}] Updating field ${name}:"${value}"...`);
+
+                            return this.graphQLClient.setItemValue(
+                                this.projectId,
+                                issueNodeId,
+                                fieldId,
+                                valueOrOptionId,
+                            );
+                        }, RETRY_CONF);
+                        this.log(`[${issueNodeId}] Successfully updated field ${name}:"${value}"`);
+                    }
+
+                    this.log(
+                        `[${issueNodeId}] Successfully recorded test result for "${test.title}"`,
+                    );
+                } catch (error) {
+                    this.logError(`Failed to process test end for "${test.title}":`, error);
+                    // Non-Critical error, no need to rethrow
+                }
+            })(),
+        );
+    }
+
+    // Finalizes reporting when all tests are complete, waits for pending operations to finish
+    // eslint-disable-next-line require-await
+    async onEnd() {
+        this.log('All tests completed, waiting for pending operations...');
+
+        if (this.pendingOperations.length > 0) {
+            this.log(`Waiting for ${this.pendingOperations.length} pending operations to complete`);
+
+            return Promise.allSettled(this.pendingOperations)
+                .then(results => {
+                    const failed = results.filter(r => r.status === 'rejected').length;
+                    if (failed > 0) {
+                        this.logError(`${failed} operations failed`);
+                    } else {
+                        this.log('All operations completed successfully');
+                    }
+                })
+                .finally(() => {
+                    this.log('GitHub reporter finished');
+                });
+        } else {
+            this.log('No pending operations, GitHub reporter finished');
+
+            return Promise.resolve();
+        }
+    }
+
+    private async initializeProject(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.log(`Using created project: ${createdProject.title} (${createdProject.id})`);
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
+        try {
+            const projects = await this.graphQLClient.getProjectFromOrganization(
+                ORGANIZATION,
+                PROJECT_NAME,
+            );
+
+            const matchingProject = projects.find((project: any) => project.title === PROJECT_NAME);
+
+            if (matchingProject) {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Adopted
created_at: 2025-04-25T06:31:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _projectId: string | undefined;
+    private _graphQLClient: GitHubGraphQLClient | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get graphQLClient(): GitHubGraphQLClient {
+        if (!this._graphQLClient) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._graphQLClient;
+    }
+
+    private get projectId(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._graphQLClient = new GitHubGraphQLClient(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.initializeProject();
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
+
+    // Processes test completion by creating a GitHub issue with test results and metadata
+    // eslint-disable-next-line require-await
+    async onTestEnd(test: TestCase) {
+        this.log(`Processing test end for "${test.title}"`);
+
+        return this.trackOperation(
+            (async () => {
+                if (this.initializationPromise) {
+                    // Wait for OnBegin to finish
+                    await this.initializationPromise;
+                }
+
+                const report = new TestReportProvider(test);
+
+                try {
+                    const issueNodeId = await scheduleAction(() => {
+                        this.log(`Creating GitHub draft issue for test "${test.title}"...`);
+
+                        return this.graphQLClient.createDraftIssueInProject(
+                            this.projectId,
+                            report.testCase,
+                            report.bodyDescription,
+                        );
+                    }, RETRY_CONF);
+                    this.log(`[${issueNodeId}] Successfully created issue "${test.title}"`);
+
+                    const fields = await scheduleAction(() => this.getProjectFields(), RETRY_CONF);
+
+                    for (const { name, value } of report.projectValues) {
+                        const { fieldId, valueOrOptionId } = this.resolveFieldAndValue(
+                            fields,
+                            name,
+                            value,
+                        );
+                        await scheduleAction(() => {
+                            this.log(`[${issueNodeId}] Updating field ${name}:"${value}"...`);
+
+                            return this.graphQLClient.setItemValue(
+                                this.projectId,
+                                issueNodeId,
+                                fieldId,
+                                valueOrOptionId,
+                            );
+                        }, RETRY_CONF);
+                        this.log(`[${issueNodeId}] Successfully updated field ${name}:"${value}"`);
+                    }
+
+                    this.log(
+                        `[${issueNodeId}] Successfully recorded test result for "${test.title}"`,
+                    );
+                } catch (error) {
+                    this.logError(`Failed to process test end for "${test.title}":`, error);
+                    // Non-Critical error, no need to rethrow
+                }
+            })(),
+        );
+    }
+
+    // Finalizes reporting when all tests are complete, waits for pending operations to finish
+    // eslint-disable-next-line require-await
+    async onEnd() {
+        this.log('All tests completed, waiting for pending operations...');
+
+        if (this.pendingOperations.length > 0) {
+            this.log(`Waiting for ${this.pendingOperations.length} pending operations to complete`);
+
+            return Promise.allSettled(this.pendingOperations)
+                .then(results => {
+                    const failed = results.filter(r => r.status === 'rejected').length;
+                    if (failed > 0) {
+                        this.logError(`${failed} operations failed`);
+                    } else {
+                        this.log('All operations completed successfully');
+                    }
+                })
+                .finally(() => {
+                    this.log('GitHub reporter finished');
+                });
+        } else {
+            this.log('No pending operations, GitHub reporter finished');
+
+            return Promise.resolve();
+        }
+    }
+
+    private async initializeProject(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.log(`Using created project: ${createdProject.title} (${createdProject.id})`);
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Adopted, requests been split
created_at: 2025-04-25T06:32:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
+              body: "${body.replace(/"/g, '\\"')}"
+            }
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: Vere-Grey
body: wow, seems I lost somewhere this change that I already adopted. i will reintroduce this
created_at: 2025-04-25T06:33:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
+              body: "${body.replace(/"/g, '\\"')}"
+            }
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: coderabbitai[bot]
body: @Vere-Grey That's a good plan! Using GraphQL variables instead of string interpolation is definitely the way to go. Let me know if you need any assistance with implementing the change.

(⌐■_■)ウサギ


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey That's a good plan! Using GraphQL variables instead of string interpolation is definitely the way to go. Let me know if you need any assistance with implementing the change.

(⌐■_■)ウサギ</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:34:11Z
author_association: NONE
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: Vere-Grey
body: already implemented
created_at: 2025-04-25T06:38:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: coderabbitai[bot]
body: @Vere-Grey I see that you mentioned you've already implemented the type-only import. However, I'm noticing that the file content still shows `import { Octokit } from '@octokit/rest';` without the `type` keyword. 

Did you implement this change in a different commit that's not yet reflected in this PR, or perhaps in another file like `GitHubReporter.ts` (which I see from our learnings uses the type-only import pattern)?

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts:1-1
Timestamp: 2025-04-25T06:39:20.248Z
Learning: The `@octokit/rest` package should be imported as a type-only dependency in files that need just the Octokit type, using `import type { Octokit } from '@octokit/rest';` to prevent runtime ESM resolution errors. This is especially important since the actual implementation is dynamically imported at runtime in GitHubReporter.ts.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey I see that you mentioned you've already implemented the type-only import. However, I'm noticing that the file content still shows `import { Octokit } from '@octokit/rest';` without the `type` keyword. 

Did you implement this change in a different commit that's not yet reflected in this PR, or perhaps in another file like `GitHubReporter.ts` (which I see from our learnings uses the type-only import pattern)?

---

<details>
<summary>✏️ Learnings added</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts:1-1
Timestamp: 2025-04-25T06:39:20.248Z
Learning: The `@octokit/rest` package should be imported as a type-only dependency in files that need just the Octokit type, using `import type { Octokit } from '@octokit/rest';` to prevent runtime ESM resolution errors. This is especially important since the actual implementation is dynamically imported at runtime in GitHubReporter.ts.
```

> Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure [review instructions](https://docs.coderabbit.ai/guides/review-instructions). You can manage existing learnings in the [app](https://app.coderabbit.ai/learnings).

</details>



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:39:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,95 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Discarded API response – missed opportunity for verification & logging**  

`updateProjectV2ItemFieldValue` returns data that could confirm the update or give precise error details.  Consider at least logging the returned `projectV2Item.id` and wrapping the call in a `try/catch` to propagate meaningful errors to callers.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-25T06:40:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,95 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**String-interpolated GraphQL opens the door to injection & escaping bugs**  

The `addIssueToProject` mutation is built by concatenating run-time values straight into the query string. Any double quote, back-slash or line-break inside `projectId` / `issueNodeId` will break the query and could be crafted into a malicious mutation.  
Use GraphQL variables instead:

```diff
-const mutation = `
-  mutation {
-    addProjectV2ItemById(
-      input: {
-        projectId: "${projectId}"
-        contentId: "${issueNodeId}"
-      }) {
-        item { id }
-    }
-  }
-`;
-const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+const mutation = `
+  mutation addIssue($projectId:ID!, $contentId:ID!) {
+    addProjectV2ItemById(input:{ projectId:$projectId, contentId:$contentId }) {
+      item { id }
+    }
+  }
+`;
+const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation, {
+  projectId,
+  contentId: issueNodeId,
+});
```
This pattern eliminates the need for ad-hoc escaping and guards against injection.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
-        const mutation = `
-            mutation {
-              addProjectV2ItemById(
-              input: {
-                projectId: "${projectId}"
-                contentId: "${issueNodeId}"
-              }) {
-                item {
-                  id
-                }
-              }
-            }
-        `;
-        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+        const mutation = `
+            mutation addIssue($projectId:ID!, $contentId:ID!) {
+              addProjectV2ItemById(input:{ projectId:$projectId, contentId:$contentId }) {
+                item { id }
+              }
+            }
+        `;
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation, {
+            projectId,
+            contentId: issueNodeId,
+        });
 
         // …rest of method…
     }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-25T06:40:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,95 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`valueOrOptionId` is interpolated raw – likely to break the mutation**  

If `valueOrOptionId` is a string, GraphQL needs it quoted; if it’s an object (`{ optionId: '...' }`) it will be stringified into `[object Object]`.  Either way the current mutation becomes invalid. Switch to variables or, at minimum, stringify the value correctly.

```diff
-              value: ${valueOrOptionId}
+              value: $value
```

…and pass `value` as a GraphQL variable (see previous comment).  
Failing to fix this will surface as a cryptic “Parse error on input” from the API.



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:40:33Z
author_association: NONE
diff_hunk: @@ -0,0 +1,279 @@
+import type { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubProject } from './gitHubProject';
+import { IssueRequests } from './issueRequests';
+import { LoggingFunctions, ProjectField } from './types';
+
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+enum InitializationState {
+    NOT_STARTED = 'NOT_STARTED',
+    IN_PROGRESS = 'IN_PROGRESS',
+    COMPLETED = 'COMPLETED',
+    FAILED = 'FAILED',
+}
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _issueRequests: IssueRequests | null = null;
+    private _gitHubProject: GitHubProject | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initState: InitializationState = InitializationState.NOT_STARTED;
+
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get issueRequests(): IssueRequests {
+        if (!this._issueRequests) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._issueRequests;
+    }
+
+    private get gitHubProject(): GitHubProject {
+        if (!this._gitHubProject) {
+            throw new Error('GitHub project is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._gitHubProject;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        this.initState = InitializationState.IN_PROGRESS;
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._issueRequests = new IssueRequests(this.octokit);
+                this._gitHubProject = new GitHubProject(this.octokit, this);
+                this.initState = InitializationState.COMPLETED;
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.initState = InitializationState.FAILED;
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.gitHubProject.init();
+        })();
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: Thanks, adopted.
created_at: 2025-04-25T06:41:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,279 @@
+import type { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubProject } from './gitHubProject';
+import { IssueRequests } from './issueRequests';
+import { LoggingFunctions, ProjectField } from './types';
+
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+enum InitializationState {
+    NOT_STARTED = 'NOT_STARTED',
+    IN_PROGRESS = 'IN_PROGRESS',
+    COMPLETED = 'COMPLETED',
+    FAILED = 'FAILED',
+}
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _issueRequests: IssueRequests | null = null;
+    private _gitHubProject: GitHubProject | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initState: InitializationState = InitializationState.NOT_STARTED;
+
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get issueRequests(): IssueRequests {
+        if (!this._issueRequests) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._issueRequests;
+    }
+
+    private get gitHubProject(): GitHubProject {
+        if (!this._gitHubProject) {
+            throw new Error('GitHub project is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._gitHubProject;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        this.initState = InitializationState.IN_PROGRESS;
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._issueRequests = new IssueRequests(this.octokit);
+                this._gitHubProject = new GitHubProject(this.octokit, this);
+                this.initState = InitializationState.COMPLETED;
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.initState = InitializationState.FAILED;
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.gitHubProject.init();
+        })();
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: coderabbitai[bot]
body: @Vere-Grey You're welcome! I'm glad the fix for the race condition was helpful. This change will ensure the initialization state is properly managed, preventing potential errors from occurring when tests run in quick succession.



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

</details>

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20race%20conditions%20by%20updating%20initState%20only%20after%20async%20project%20init%20completes!%20Ensures%20stable%20GitHub%20integration%20without%20premature%20state%20flips.%20Try%20%40coderabbitai%20for%20smarter%20code%20reviews!%20%F0%9F%9A%80%F0%9F%90%87&url=https%3A%2F%2Fgithub.com%2Ftrezor%2Ftrezor-suite%2Fpull%2F18505%23discussion_r2059634276)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey You're welcome! I'm glad the fix for the race condition was helpful. This change will ensure the initialization state is properly managed, preventing potential errors from occurring when tests run in quick succession.



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

</details>

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20race%20conditions%20by%20updating%20initState%20only%20after%20async%20project%20init%20completes!%20Ensures%20stable%20GitHub%20integration%20without%20premature%20state%20flips.%20Try%20%40coderabbitai%20for%20smarter%20code%20reviews!%20%F0%9F%9A%80%F0%9F%90%87&url=https%3A%2F%2Fgithub.com%2Ftrezor%2Ftrezor-suite%2Fpull%2F18505%23discussion_r2059634276)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:42:09Z
author_association: NONE
diff_hunk: @@ -0,0 +1,165 @@
+import { Octokit } from '@octokit/rest';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { ProjectRequests } from './projectRequests';
+import { LoggingFunctions } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 37'; // TODO: Get this from CI, probably build name
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+export class GitHubProject {
+    private _projectId: string | undefined;
+    private graphQLClient: ProjectRequests;
+
+    constructor(
+        octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {
+        this.graphQLClient = new ProjectRequests(octokit, logger);
+    }
+
+    get id(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    async init(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.logger.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.logger.log(
+                    `Using created project: ${createdProject.title} (${createdProject.id})`,
+                );
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logger.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
+        try {
+            const projects = await scheduleAction(
+                () => this.graphQLClient.getProjectFromOrganization(ORGANIZATION, PROJECT_NAME),
+                RETRY_CONF,
+            );
+
+            const matchingProject = projects.find((project: any) => project.title === PROJECT_NAME);
+
+            if (!matchingProject) {
+                return null;
+            }
+
+            const areThereDuplicates =
+                projects.filter((project: any) => project.title === PROJECT_NAME).length > 1;
+            if (areThereDuplicates) {
+                this.logger.log(
+                    `Warning: Multiple projects found with title "${PROJECT_NAME}". Using the first one with number ${matchingProject.number}.`,
+                );
+            }
+
path: packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
user: login: Vere-Grey
body: GraphQL response is ordered by number, which is github ordered id of projects
But thanks for warning
Not a problem
created_at: 2025-04-25T06:43:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+import { Octokit } from '@octokit/rest';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { ProjectRequests } from './projectRequests';
+import { LoggingFunctions } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 37'; // TODO: Get this from CI, probably build name
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+export class GitHubProject {
+    private _projectId: string | undefined;
+    private graphQLClient: ProjectRequests;
+
+    constructor(
+        octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {
+        this.graphQLClient = new ProjectRequests(octokit, logger);
+    }
+
+    get id(): string {
+        if (!this._projectId) {
+            throw new Error('Project ID is not set. Ensure onBegin() is called first.');
+        }
+
+        return this._projectId;
+    }
+
+    async init(): Promise<void> {
+        try {
+            const existingProject = await this.findExistingProject();
+
+            if (existingProject) {
+                this._projectId = existingProject.id;
+                this.logger.log(
+                    `Using existing project: ${existingProject.title} (${existingProject.id})`,
+                );
+
+                return;
+            }
+
+            await this.createProject(annotationsForProjectFields);
+
+            // Instead of taking projectId from 'createProject()' we run another 'findExistingProject()' query again
+            // Goal is to avoid conflicts by searching for the project again, by its name and choosing the oldest
+            // Source of conflict: Parallel workflows on CI (Web x Desktop), parallel groups in one workflow
+            const createdProject = await this.findExistingProject();
+            if (createdProject) {
+                this._projectId = createdProject.id;
+                this.logger.log(
+                    `Using created project: ${createdProject.title} (${createdProject.id})`,
+                );
+
+                return;
+            } else {
+                throw new Error('Failed to find the created project');
+            }
+        } catch (error) {
+            this.logger.logError('Project initialization failed.');
+            throw error;
+        }
+    }
+
+    private async findExistingProject(): Promise<{ id: string; title: string } | null> {
+        try {
+            const projects = await scheduleAction(
+                () => this.graphQLClient.getProjectFromOrganization(ORGANIZATION, PROJECT_NAME),
+                RETRY_CONF,
+            );
+
+            const matchingProject = projects.find((project: any) => project.title === PROJECT_NAME);
+
+            if (!matchingProject) {
+                return null;
+            }
+
+            const areThereDuplicates =
+                projects.filter((project: any) => project.title === PROJECT_NAME).length > 1;
+            if (areThereDuplicates) {
+                this.logger.log(
+                    `Warning: Multiple projects found with title "${PROJECT_NAME}". Using the first one with number ${matchingProject.number}.`,
+                );
+            }
+
path: packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
user: login: coderabbitai[bot]
body: @Vere-Grey Thanks for clarifying that the GraphQL response is already ordered by project number! This explains why your current implementation works well for selecting the first matching project when there are duplicates. I appreciate you sharing this insight about GitHub's API behavior.

---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey Thanks for clarifying that the GraphQL response is already ordered by project number! This explains why your current implementation works well for selecting the first matching project when there are duplicates. I appreciate you sharing this insight about GitHub's API behavior.

---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:44:09Z
author_association: NONE
diff_hunk: @@ -0,0 +1,279 @@
+import type { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubProject } from './gitHubProject';
+import { IssueRequests } from './issueRequests';
+import { LoggingFunctions, ProjectField } from './types';
+
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+enum InitializationState {
+    NOT_STARTED = 'NOT_STARTED',
+    IN_PROGRESS = 'IN_PROGRESS',
+    COMPLETED = 'COMPLETED',
+    FAILED = 'FAILED',
+}
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _issueRequests: IssueRequests | null = null;
+    private _gitHubProject: GitHubProject | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initState: InitializationState = InitializationState.NOT_STARTED;
+
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get issueRequests(): IssueRequests {
+        if (!this._issueRequests) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._issueRequests;
+    }
+
+    private get gitHubProject(): GitHubProject {
+        if (!this._gitHubProject) {
+            throw new Error('GitHub project is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._gitHubProject;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        this.initState = InitializationState.IN_PROGRESS;
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._issueRequests = new IssueRequests(this.octokit);
+                this._gitHubProject = new GitHubProject(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.initState = InitializationState.FAILED;
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.gitHubProject.init();
+            this.initState = InitializationState.COMPLETED;
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add retry logic to project initialization**

While you've implemented retry logic in `onTestEnd` with `scheduleAction`, the critical project initialization in `onBegin` doesn't have the same protection against transient failures.


```diff
 async onBegin() {
     this.initState = InitializationState.IN_PROGRESS;
     const initPromise = (async () => {
         try {
             const OctokitModule = await import('@octokit/rest');
             this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
             this._issueRequests = new IssueRequests(this.octokit);
             this._gitHubProject = new GitHubProject(this.octokit, this);
             this.log('GitHub client initialized successfully');
-            
-        } catch (error) {
-            this.initState = InitializationState.FAILED;
-            this.logError('Failed to initialize GitHub reporter.');
-            throw error; // Critical error, rethrow to stop execution
-        }
-
-        await this.gitHubProject.init();
-        this.initState = InitializationState.COMPLETED;
+        } catch (error) {
+            this.initState = InitializationState.FAILED;
+            this.logError('Failed to initialize GitHub reporter.');
+            throw error; // Critical error, rethrow to stop execution
+        }
+
+        await scheduleAction(() => this.gitHubProject.init(), RETRY_CONF);
+        this.initState = InitializationState.COMPLETED;
     })();
     this.initializationPromise = initPromise;

     return this.trackOperation(initPromise);
 }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
    // eslint-disable-next-line require-await
    async onBegin() {
        this.initState = InitializationState.IN_PROGRESS;
        const initPromise = (async () => {
            try {
                const OctokitModule = await import('@octokit/rest');
                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
                this._issueRequests = new IssueRequests(this.octokit);
                this._gitHubProject = new GitHubProject(this.octokit, this);
                this.log('GitHub client initialized successfully');
            } catch (error) {
                this.initState = InitializationState.FAILED;
                this.logError('Failed to initialize GitHub reporter.');
                throw error; // Critical error, rethrow to stop execution
            }

            await scheduleAction(() => this.gitHubProject.init(), RETRY_CONF);
            this.initState = InitializationState.COMPLETED;
        })();
        this.initializationPromise = initPromise;

        return this.trackOperation(initPromise);
    }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:44:36Z
author_association: NONE
diff_hunk: @@ -0,0 +1,165 @@
+import { Octokit } from '@octokit/rest';
path: packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
user: login: Vere-Grey
body: ahh, good catch.
adopted
created_at: 2025-04-25T06:44:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,165 @@
+import { Octokit } from '@octokit/rest';
path: packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts
user: login: coderabbitai[bot]
body: @Vere-Grey Great! I'm glad you adopted the suggestion to use type-only imports for Octokit. This change aligns perfectly with the existing pattern in your codebase and helps prevent runtime ESM resolution errors.



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

---

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

---

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts:1-1
Timestamp: 2025-04-25T06:39:20.248Z
Learning: The `@octokit/rest` package should be imported as a type-only dependency in files that need just the Octokit type, using `import type { Octokit } from '@octokit/rest';` to prevent runtime ESM resolution errors. This is especially important since the actual implementation is dynamically imported at runtime in GitHubReporter.ts.
```

</details>

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Optimize%20imports%20by%20using%20type-only%20for%20Octokit%20when%20runtime%20value%20isn%E2%80%99t%20needed!%20%F0%9F%9A%80%20Reduces%20bundle%20size%20%26%20avoids%20double%20loading.%20Try%20%40coderabbitai%20for%20smart%2C%20precise%20code%20reviews!%20%F0%9F%9B%A0%EF%B8%8F&url=https%3A%2F%2Fgithub.com%2Ftrezor%2Ftrezor-suite%2Fpull%2F18505%23discussion_r2059634289)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey Great! I'm glad you adopted the suggestion to use type-only imports for Octokit. This change aligns perfectly with the existing pattern in your codebase and helps prevent runtime ESM resolution errors.



---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

---

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:14-18
Timestamp: 2025-04-24T19:16:37.002Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts has hardcoded PROJECT_NAME and VERBOSE values intentionally. These will be made configurable through environment variables in a follow-up PR that introduces the workflow part.
```

---

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts:1-1
Timestamp: 2025-04-25T06:39:20.248Z
Learning: The `@octokit/rest` package should be imported as a type-only dependency in files that need just the Octokit type, using `import type { Octokit } from '@octokit/rest';` to prevent runtime ESM resolution errors. This is especially important since the actual implementation is dynamically imported at runtime in GitHubReporter.ts.
```

</details>

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Optimize%20imports%20by%20using%20type-only%20for%20Octokit%20when%20runtime%20value%20isn%E2%80%99t%20needed!%20%F0%9F%9A%80%20Reduces%20bundle%20size%20%26%20avoids%20double%20loading.%20Try%20%40coderabbitai%20for%20smart%2C%20precise%20code%20reviews!%20%F0%9F%9B%A0%EF%B8%8F&url=https%3A%2F%2Fgithub.com%2Ftrezor%2Ftrezor-suite%2Fpull%2F18505%23discussion_r2059634289)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:45:22Z
author_association: NONE
diff_hunk: @@ -0,0 +1,190 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+} from './types';
+
+export async function getProjectFields(
+    octokit: Octokit,
+    projectId: string,
+): Promise<ProjectField[]> {
+    const query = `
+        query {
+          node(id: "${projectId}") {
+            ... on ProjectV2 {
+              fields(first: 20) {
+                nodes {
+                  ... on ProjectV2FieldCommon {
+                    id
+                    name
+                    dataType
+                  }
+                  ... on ProjectV2SingleSelectField {
+                    id
+                    name
+                    dataType
+                    options {
+                      id
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    `;
+
+    const response = await octokit.graphql<ProjectFieldsResponse>(query);
+
+    return response.node.fields.nodes;
+}
path: packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
user: login: Vere-Grey
body: This is systematic question we are discussing on another comment. Closing this one
created_at: 2025-04-25T06:45:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+} from './types';
+
+export async function getProjectFields(
+    octokit: Octokit,
+    projectId: string,
+): Promise<ProjectField[]> {
+    const query = `
+        query {
+          node(id: "${projectId}") {
+            ... on ProjectV2 {
+              fields(first: 20) {
+                nodes {
+                  ... on ProjectV2FieldCommon {
+                    id
+                    name
+                    dataType
+                  }
+                  ... on ProjectV2SingleSelectField {
+                    id
+                    name
+                    dataType
+                    options {
+                      id
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    `;
+
+    const response = await octokit.graphql<ProjectFieldsResponse>(query);
+
+    return response.node.fields.nodes;
+}
path: packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
user: login: coderabbitai[bot]
body: @Vere-Grey I understand that this topic is being addressed in another comment thread, so we can close this discussion. Thanks for letting me know.


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey I understand that this topic is being addressed in another comment thread, so we can close this discussion. Thanks for letting me know.</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:45:52Z
author_association: NONE
diff_hunk: @@ -0,0 +1,190 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+} from './types';
+
+export async function getProjectFields(
+    octokit: Octokit,
+    projectId: string,
+): Promise<ProjectField[]> {
+    const query = `
+        query {
+          node(id: "${projectId}") {
+            ... on ProjectV2 {
+              fields(first: 20) {
+                nodes {
+                  ... on ProjectV2FieldCommon {
+                    id
+                    name
+                    dataType
+                  }
+                  ... on ProjectV2SingleSelectField {
+                    id
+                    name
+                    dataType
+                    options {
+                      id
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    `;
+
+    const response = await octokit.graphql<ProjectFieldsResponse>(query);
+
+    return response.node.fields.nodes;
+}
+
+export class ProjectRequests {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
path: packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
user: login: Vere-Grey
body: Already implemented on caller level
created_at: 2025-04-25T06:47:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: Vere-Grey
body: Adopted
created_at: 2025-04-25T06:47:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,91 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
+              projectV2Item {
+                id
+              }
+            }
+          }
+      `;
+
+        await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
+    }
+
+    async createDraftIssueInProject(
+        projectId: string,
+        title: string,
+        body: string,
+    ): Promise<string> {
+        const mutation = `
+        mutation {
+          addProjectV2DraftIssue(
+            input: {
+              assigneeIds: []
+              projectId: "${projectId}"
+              title: "${title.replace(/"/g, '\\"')}"
+              body: "${body.replace(/"/g, '\\"')}"
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: Vere-Grey
body: adopted
created_at: 2025-04-25T06:47:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,190 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+} from './types';
+
+export async function getProjectFields(
+    octokit: Octokit,
+    projectId: string,
+): Promise<ProjectField[]> {
+    const query = `
+        query {
+          node(id: "${projectId}") {
+            ... on ProjectV2 {
+              fields(first: 20) {
+                nodes {
+                  ... on ProjectV2FieldCommon {
+                    id
+                    name
+                    dataType
+                  }
+                  ... on ProjectV2SingleSelectField {
+                    id
+                    name
+                    dataType
+                    options {
+                      id
+                      name
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+    `;
+
+    const response = await octokit.graphql<ProjectFieldsResponse>(query);
+
+    return response.node.fields.nodes;
+}
+
+export class ProjectRequests {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
path: packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

@Vere-Grey Thank you for confirming that this validation is already handled at the caller level. I'll examine the caller implementation to verify this is the case.

---

🏁 Script executed:
```
#!/bin/bash
# Check if the GitHubProject class calls createProject and implements the validation logic
cat packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts | grep -A 30 "createProject" | head -n 40
```

Length of output: 1920

---

@Vere-Grey You're absolutely right. I can see that the validation and duplicate project check is already robustly implemented at the caller level in the `GitHubProject` class. Your implementation actually goes beyond simple validation by also handling potential race conditions from parallel CI workflows.

The approach you've taken - checking for existing projects first, creating if needed, then checking again to handle potential duplicates created by parallel workflows, and choosing the oldest one - is an elegant solution to manage concurrency issues without requiring complex synchronization mechanisms.

This is indeed more sophisticated than the suggested approach and properly handles the real-world scenario of concurrent workflows potentially creating duplicate projects.

---

🏁 Script executed:
```
#!/bin/bash
# Let's see the full GitHub project init method implementation to verify the validation and duplicate handling
cat packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts | grep -A 50 "init()" | head -n 70
```

Length of output: 2127

---

@Vere-Grey You're absolutely right. Looking at the implementation, I can see that project validation and duplicate checking is already robustly implemented at the caller level in the `GitHubProject` class. 

Your approach is actually more sophisticated than what was suggested:

1. First checking if a project already exists via `findExistingProject()`
2. Creating a project only if needed
3. Then cleverly running `findExistingProject()` again after creation to handle potential race conditions from parallel CI workflows
4. Including thorough error handling and logging throughout

This implementation provides better separation of concerns - keeping the `ProjectRequests` class focused on API operations while the caller handles the business logic around duplication and validation.

---

🏁 Script executed:
```
#!/bin/bash
# Let's also check the findExistingProject method to see how duplicates are handled
cat packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts | grep -A 25 "areThereDuplicates" | head -n 25
```

Length of output: 1151

---



</details>

@Vere-Grey I see what you mean now. Looking at the code, your implementation is indeed robust and well-designed:

1. In the `init()` method, you first check if a project exists before creating one
2. After creation, you intelligently query again with `findExistingProject()` to handle race conditions from parallel CI workflows
3. The `findExistingProject()` method properly detects and handles duplicate projects
4. You've included thorough error handling and logging throughout

Your approach is actually more comprehensive than the suggested implementation, with better separation of concerns - keeping the `ProjectRequests` class focused on API operations while the caller (`GitHubProject`) handles the business logic around duplicate detection and validation.

The solution elegantly handles concurrent CI workflows that might create duplicate projects without requiring complex synchronization mechanisms.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippets from `packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts` (lines 35-37)
```ts
getProjectFields(projectId: string): Promise<ProjectField[]> {
        return getProjectFields(this.octokit, projectId);
    }
```
- This method calls the standalone `getProjectFields` function with an Octokit instance and a project ID, returning a promise resolving to an array of `ProjectField`. It shows a caller-level implementation of `getProjectFields`.

---

### Relevant snippets from `packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts` (lines 197-210)
```ts
private async getProjectFields(): Promise<ProjectField[]> {
        if (this.cachedFields) {
            this.log('Using cached project fields');

            return this.cachedFields;
        }

        this.log(`Fetching fields for project ${this.gitHubProject.id}...`);
        const fields = await this.issueRequests.getProjectFields(this.gitHubProject.id);
        this.log(`Successfully retrieved fields for project ${this.gitHubProject.id}`);
        this.cachedFields = fields;

        return fields;
    }
```
- This method fetches project fields using a cached value if available; otherwise, it calls `issueRequests.getProjectFields` with the project ID. This is an example of a caller-level implementation that wraps the lower-level request.

---

### Relevant snippets from `packages/suite-desktop-core/e2e/support/reporters/types.ts`

- `ProjectField` interface (lines 43-48):
```ts
export interface ProjectField {
    id: string;
    name: string;
    dataType?: string;
    options?: Array<FieldOption>;
}
```
- Defines the shape of a project field object, used in the `getProjectFields` return type.

- `ProjectFieldsResponse` interface (lines 50-56):
```ts
export interface ProjectFieldsResponse {
    node: {
        fields: {
            nodes: ProjectField[];
        };
    };
}
```
- Defines the GraphQL response structure for the `getProjectFields` query.

- `LoggingFunctions` interface (lines 1-5):
```ts
export interface LoggingFunctions {
    log: (...args: any[]) => void;
    logError: (...args: any[]) => void;
    logResponse: (label: string, response: any) => void;
}
```
- Defines the logging functions used by `ProjectRequests`.

- `CreateProjectMutation` interface (lines 14-20):
```ts
export interface CreateProjectMutation {
    createProjectV2: {
        projectV2: {
            id: string;
        };
    };
}
```
- Defines the GraphQL mutation response for creating a project.

- `CreateFieldResponse` interface (lines 66-72):
```ts
export interface CreateFieldResponse {
    createProjectV2Field: {
        projectV2Field: {
            id: string;
        };
    };
}
```
- Defines the GraphQL mutation response for creating a project field.

- `Project` interface (lines 7-11):
```ts
export interface Project {
    id: string;
    title: string;
    number: number;
}
```
- Defines the shape of a project object.

- `ProjectQueryResponse` interface (lines 22-28):
```ts
export interface ProjectQueryResponse {
    organization: {
        projectsV2: {
            nodes: Project[];
        };
    };
}
```
- Defines the GraphQL query response structure for fetching projects from an organization.

---

### Summary of relevance:
- The snippet from `issueRequests.ts` shows a caller-level method that calls the standalone `getProjectFields` function, directly relevant to the comment "Already implemented on caller level."
- The snippet from `gitHubReporter.ts` shows a higher-level caller method that uses caching and calls `issueRequests.getProjectFields`, illustrating another caller-level implementation.
- The type definitions from `types.ts` provide context on the data structures and GraphQL responses used in `projectRequests.ts`.

No other snippets are directly relevant to the comment or the file's functionality.

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

@Vere-Grey Thank you for confirming that this validation is already handled at the caller level. I'll examine the caller implementation to verify this is the case.

---

🏁 Script executed:
```
#!/bin/bash
# Check if the GitHubProject class calls createProject and implements the validation logic
cat packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts | grep -A 30 "createProject" | head -n 40
```

Length of output: 1920

---

@Vere-Grey You're absolutely right. I can see that the validation and duplicate project check is already robustly implemented at the caller level in the `GitHubProject` class. Your implementation actually goes beyond simple validation by also handling potential race conditions from parallel CI workflows.

The approach you've taken - checking for existing projects first, creating if needed, then checking again to handle potential duplicates created by parallel workflows, and choosing the oldest one - is an elegant solution to manage concurrency issues without requiring complex synchronization mechanisms.

This is indeed more sophisticated than the suggested approach and properly handles the real-world scenario of concurrent workflows potentially creating duplicate projects.

---

🏁 Script executed:
```
#!/bin/bash
# Let's see the full GitHub project init method implementation to verify the validation and duplicate handling
cat packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts | grep -A 50 "init()" | head -n 70
```

Length of output: 2127

---

@Vere-Grey You're absolutely right. Looking at the implementation, I can see that project validation and duplicate checking is already robustly implemented at the caller level in the `GitHubProject` class. 

Your approach is actually more sophisticated than what was suggested:

1. First checking if a project already exists via `findExistingProject()`
2. Creating a project only if needed
3. Then cleverly running `findExistingProject()` again after creation to handle potential race conditions from parallel CI workflows
4. Including thorough error handling and logging throughout

This implementation provides better separation of concerns - keeping the `ProjectRequests` class focused on API operations while the caller handles the business logic around duplication and validation.

---

🏁 Script executed:
```
#!/bin/bash
# Let's also check the findExistingProject method to see how duplicates are handled
cat packages/suite-desktop-core/e2e/support/reporters/gitHubProject.ts | grep -A 25 "areThereDuplicates" | head -n 25
```

Length of output: 1151

---



</details>

@Vere-Grey I see what you mean now. Looking at the code, your implementation is indeed robust and well-designed:

1. In the `init()` method, you first check if a project exists before creating one
2. After creation, you intelligently query again with `findExistingProject()` to handle race conditions from parallel CI workflows
3. The `findExistingProject()` method properly detects and handles duplicate projects
4. You've included thorough error handling and logging throughout

Your approach is actually more comprehensive than the suggested implementation, with better separation of concerns - keeping the `ProjectRequests` class focused on API operations while the caller (`GitHubProject`) handles the business logic around duplicate detection and validation.

The solution elegantly handles concurrent CI workflows that might create duplicate projects without requiring complex synchronization mechanisms.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippets from `packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts` (lines 35-37)
```ts
getProjectFields(projectId: string): Promise<ProjectField[]> {
        return getProjectFields(this.octokit, projectId);
    }
```
- This method calls the standalone `getProjectFields` function with an Octokit instance and a project ID, returning a promise resolving to an array of `ProjectField`. It shows a caller-level implementation of `getProjectFields`.

---

### Relevant snippets from `packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts` (lines 197-210)
```ts
private async getProjectFields(): Promise<ProjectField[]> {
        if (this.cachedFields) {
            this.log('Using cached project fields');

            return this.cachedFields;
        }

        this.log(`Fetching fields for project ${this.gitHubProject.id}...`);
        const fields = await this.issueRequests.getProjectFields(this.gitHubProject.id);
        this.log(`Successfully retrieved fields for project ${this.gitHubProject.id}`);
        this.cachedFields = fields;

        return fields;
    }
```
- This method fetches project fields using a cached value if available; otherwise, it calls `issueRequests.getProjectFields` with the project ID. This is an example of a caller-level implementation that wraps the lower-level request.

---

### Relevant snippets from `packages/suite-desktop-core/e2e/support/reporters/types.ts`

- `ProjectField` interface (lines 43-48):
```ts
export interface ProjectField {
    id: string;
    name: string;
    dataType?: string;
    options?: Array<FieldOption>;
}
```
- Defines the shape of a project field object, used in the `getProjectFields` return type.

- `ProjectFieldsResponse` interface (lines 50-56):
```ts
export interface ProjectFieldsResponse {
    node: {
        fields: {
            nodes: ProjectField[];
        };
    };
}
```
- Defines the GraphQL response structure for the `getProjectFields` query.

- `LoggingFunctions` interface (lines 1-5):
```ts
export interface LoggingFunctions {
    log: (...args: any[]) => void;
    logError: (...args: any[]) => void;
    logResponse: (label: string, response: any) => void;
}
```
- Defines the logging functions used by `ProjectRequests`.

- `CreateProjectMutation` interface (lines 14-20):
```ts
export interface CreateProjectMutation {
    createProjectV2: {
        projectV2: {
            id: string;
        };
    };
}
```
- Defines the GraphQL mutation response for creating a project.

- `CreateFieldResponse` interface (lines 66-72):
```ts
export interface CreateFieldResponse {
    createProjectV2Field: {
        projectV2Field: {
            id: string;
        };
    };
}
```
- Defines the GraphQL mutation response for creating a project field.

- `Project` interface (lines 7-11):
```ts
export interface Project {
    id: string;
    title: string;
    number: number;
}
```
- Defines the shape of a project object.

- `ProjectQueryResponse` interface (lines 22-28):
```ts
export interface ProjectQueryResponse {
    organization: {
        projectsV2: {
            nodes: Project[];
        };
    };
}
```
- Defines the GraphQL query response structure for fetching projects from an organization.

---

### Summary of relevance:
- The snippet from `issueRequests.ts` shows a caller-level method that calls the standalone `getProjectFields` function, directly relevant to the comment "Already implemented on caller level."
- The snippet from `gitHubReporter.ts` shows a higher-level caller method that uses caching and calls `issueRequests.getProjectFields`, illustrating another caller-level implementation.
- The type definitions from `types.ts` provide context on the data structures and GraphQL responses used in `projectRequests.ts`.

No other snippets are directly relevant to the comment or the file's functionality.

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:49:14Z
author_association: NONE
diff_hunk: @@ -0,0 +1,279 @@
+import type { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubProject } from './gitHubProject';
+import { IssueRequests } from './issueRequests';
+import { LoggingFunctions, ProjectField } from './types';
+
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+enum InitializationState {
+    NOT_STARTED = 'NOT_STARTED',
+    IN_PROGRESS = 'IN_PROGRESS',
+    COMPLETED = 'COMPLETED',
+    FAILED = 'FAILED',
+}
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _issueRequests: IssueRequests | null = null;
+    private _gitHubProject: GitHubProject | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initState: InitializationState = InitializationState.NOT_STARTED;
+
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get issueRequests(): IssueRequests {
+        if (!this._issueRequests) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._issueRequests;
+    }
+
+    private get gitHubProject(): GitHubProject {
+        if (!this._gitHubProject) {
+            throw new Error('GitHub project is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._gitHubProject;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        this.initState = InitializationState.IN_PROGRESS;
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._issueRequests = new IssueRequests(this.octokit);
+                this._gitHubProject = new GitHubProject(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.initState = InitializationState.FAILED;
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.gitHubProject.init();
+            this.initState = InitializationState.COMPLETED;
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: done on lower levels to avoid conflicts and duplication
created_at: 2025-04-25T06:49:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,279 @@
+import type { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubProject } from './gitHubProject';
+import { IssueRequests } from './issueRequests';
+import { LoggingFunctions, ProjectField } from './types';
+
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+enum InitializationState {
+    NOT_STARTED = 'NOT_STARTED',
+    IN_PROGRESS = 'IN_PROGRESS',
+    COMPLETED = 'COMPLETED',
+    FAILED = 'FAILED',
+}
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
+    private _issueRequests: IssueRequests | null = null;
+    private _gitHubProject: GitHubProject | null = null;
+    private pendingOperations: Promise<any>[] = [];
+    private cachedFields: ProjectField[] | null = null;
+    private initState: InitializationState = InitializationState.NOT_STARTED;
+
+    private initializationPromise: Promise<void> | null = null;
+
+    log(...args: any[]): void {
+        if (VERBOSE) {
+            console.warn('[GitHub Reporter]', ...args);
+        }
+    }
+
+    logError(...args: any[]): void {
+        console.error('[GitHub Reporter ERROR]', ...args);
+    }
+
+    logResponse(label: string, response: any): void {
+        if (VERBOSE) {
+            console.warn(`[GitHub Reporter] ${label}:`);
+            console.warn(JSON.stringify(response, null, 2));
+        }
+    }
+
+    private get octokit(): Octokit {
+        if (!this._octokit) {
+            throw new Error(
+                'Octokit instance is not initialized. Ensure onBegin() is called first.',
+            );
+        }
+
+        return this._octokit;
+    }
+
+    private get issueRequests(): IssueRequests {
+        if (!this._issueRequests) {
+            throw new Error('GraphQL client is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._issueRequests;
+    }
+
+    private get gitHubProject(): GitHubProject {
+        if (!this._gitHubProject) {
+            throw new Error('GitHub project is not initialized. Ensure onBegin() is called first.');
+        }
+
+        return this._gitHubProject;
+    }
+
+    // Tracks asynchronous operations and logs their completion
+    // Otherwise, playwright would not wait for them to finish
+    private trackOperation<T>(operation: Promise<T>): Promise<T> {
+        this.pendingOperations.push(operation);
+
+        return operation.finally(() => {
+            const index = this.pendingOperations.indexOf(operation);
+            if (index !== -1) {
+                this.pendingOperations.splice(index, 1);
+                this.log(`Operation completed (${this.pendingOperations.length} remaining)`);
+            }
+        });
+    }
+
+    // Initializes the reporter when test run begins, creates a GitHub project if it doesn't exist
+    // eslint-disable-next-line require-await
+    async onBegin() {
+        this.initState = InitializationState.IN_PROGRESS;
+        const initPromise = (async () => {
+            try {
+                const OctokitModule = await import('@octokit/rest');
+                this._octokit = new OctokitModule.Octokit({ auth: process.env.GITHUB_TOKEN });
+                this._issueRequests = new IssueRequests(this.octokit);
+                this._gitHubProject = new GitHubProject(this.octokit, this);
+                this.log('GitHub client initialized successfully');
+            } catch (error) {
+                this.initState = InitializationState.FAILED;
+                this.logError('Failed to initialize GitHub reporter.');
+                throw error; // Critical error, rethrow to stop execution
+            }
+
+            await this.gitHubProject.init();
+            this.initState = InitializationState.COMPLETED;
+        })();
+        this.initializationPromise = initPromise;
+
+        return this.trackOperation(initPromise);
+    }
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: coderabbitai[bot]
body: @Vere-Grey Thanks for clarifying. That makes sense - implementing retry logic at the lower levels would be more efficient than duplicating it at multiple levels. This keeps the code cleaner and avoids potential conflicts between different retry mechanisms.

---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@Vere-Grey Thanks for clarifying. That makes sense - implementing retry logic at the lower levels would be more efficient than duplicating it at multiple levels. This keeps the code cleaner and avoids potential conflicts between different retry mechanisms.

---

<details>
<summary>🧠 Learnings used</summary>

```
Learnt from: Vere-Grey
PR: trezor/trezor-suite#18505
File: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts:1-3
Timestamp: 2025-04-24T21:02:21.431Z
Learning: The GitHub reporter implementation in packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts uses conditional exports from @octokit/rest, requiring an `import type` approach for the Octokit type while dynamically importing the actual implementation at runtime.
```

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T06:50:27Z
author_association: NONE
diff_hunk: @@ -0,0 +1,95 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: Vere-Grey
body: not a string, It is enum
created_at: 2025-04-25T10:22:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,258 @@
+import { Octokit } from '@octokit/rest';
+
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    CreateFieldResponse,
+    CreateProjectMutation,
+    LoggingFunctions,
+    Project,
+    ProjectField,
+    ProjectFieldsResponse,
+    ProjectQueryResponse,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class GitHubGraphQLClient {
+    constructor(
+        private readonly octokit: Octokit,
+        private readonly logger: LoggingFunctions,
+    ) {}
+
+    async createProject(ownerId: string, teamId: string, projectName: string): Promise<string> {
+        this.logger.log(`Creating project "${projectName}" with owner ID: ${ownerId}`);
+
+        const mutation = `
+            mutation {
+                createProjectV2(
+                input: {
+                    ownerId: "${ownerId}"
+                    title: "${projectName}"
+                    teamId: "${teamId}"
+                }) {
+                    projectV2 {
+                        id
+                    }
+                }
+            }
+        `;
+
+        const response = await this.octokit.graphql<CreateProjectMutation>(mutation);
+        const projectId = response.createProjectV2.projectV2.id;
+
+        this.logger.log(`Created GitHub project ${projectName} with ID: ${projectId}`);
+
+        return projectId;
+    }
path: packages/suite-desktop-core/e2e/support/reporters/gitHubGraphQLClient.ts
user: login: Vere-Grey
body: agreed to keep as it is
created_at: 2025-04-25T10:23:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+import { Octokit } from '@octokit/rest';
+
+import { getProjectFields } from './projectRequests';
+import {
+    AddDraftIssueResponse,
+    AddIssueToProjectResponse,
+    ProjectField,
+    UpdateProjectItemFieldResponse,
+    ValueOrOptionId,
+} from './types';
+
+export class IssueRequests {
+    constructor(private readonly octokit: Octokit) {}
+
+    async addIssueToProject(projectId: string, issueNodeId: string): Promise<string> {
+        const mutation = `
+            mutation {
+              addProjectV2ItemById(
+              input: {
+                projectId: "${projectId}"
+                contentId: "${issueNodeId}"
+              }) {
+                item {
+                  id
+                }
+              }
+            }
+        `;
+
+        const response = await this.octokit.graphql<AddIssueToProjectResponse>(mutation);
+
+        return response.addProjectV2ItemById.item.id;
+    }
+
+    getProjectFields(projectId: string): Promise<ProjectField[]> {
+        return getProjectFields(this.octokit, projectId);
+    }
+
+    async setItemValue(
+        projectId: string,
+        itemId: string,
+        fieldId: string,
+        valueOrOptionId: ValueOrOptionId,
+    ): Promise<void> {
+        const mutation = `
+          mutation {
+            updateProjectV2ItemFieldValue(
+            input: {
+              projectId: "${projectId}"
+              itemId: "${itemId}"
+              fieldId: "${fieldId}"
+              value: ${valueOrOptionId}
+            }) {
path: packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts
user: login: coderabbitai[bot]
body: <details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Let's first check the ValueOrOptionId type definition
cat packages/suite-desktop-core/e2e/support/reporters/types.ts | grep -A 5 "ValueOrOptionId"

# Now let's examine how setItemValue is called in the codebase
rg "setItemValue\(" --context=10

# Let's look at the gitHubReporter.ts file since it's likely to call this method
cat packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts | grep -A 20 -B 20 "setItemValue"
```

Length of output: 6433

---

🏁 Script executed:
```
#!/bin/bash
# Let's check the resolveFieldAndValue method in gitHubReporter.ts
cat packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts | grep -A 30 "resolveFieldAndValue"

# Let's see what the GitHub GraphQL API expects for the updateProjectV2ItemFieldValue mutation
# Check for any utility functions that might help with this
grep -r "updateProjectV2ItemFieldValue" --include="*.ts" .
```

Length of output: 2808

---



</details>

@Vere-Grey I see what you mean now! Looking at your `resolveFieldAndValue` method, I can see that you're correctly formatting the enum values as GraphQL input objects:

```typescript
// For single-select (enum) fields
valueOrOptionId: `{ singleSelectOptionId: "${option.id}" }`
```

This pre-formatted string is valid for direct interpolation into the GraphQL mutation. The bot was concerned about raw strings or objects, but your implementation already handles this properly. The current code works correctly for both text and single-select field types.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippets from `packages/suite-desktop-core/e2e/support/reporters/types.ts`

At lines 86-86:
```typescript
export type ValueOrOptionId = string | { optionId: string };
```
- This type alias defines `ValueOrOptionId` as either a string or an object with an `optionId` string property.
- It is used in `IssueRequests.setItemValue` method as the type for the `valueOrOptionId` parameter.
- This clarifies that the value passed to the GraphQL mutation can be either a raw string or an enum-like object with an `optionId`.

At lines 43-48:
```typescript
export interface ProjectField {
    id: string;
    name: string;
    dataType?: string;
    options?: Array<FieldOption>;
}
```
- Defines the structure of a project field, including optional `dataType` and `options`.
- Relevant for understanding the fields that can be set on a project item.

At lines 30-36:
```typescript
export interface AddIssueToProjectResponse {
    addProjectV2ItemById: {
        item: {
            id: string;
        };
    };
}
```
- Defines the expected response shape from the `addIssueToProject` GraphQL mutation.

At lines 58-64:
```typescript
export interface UpdateProjectItemFieldResponse {
    updateProjectV2ItemFieldValue: {
        projectV2Item: {
            id: string;
        };
    };
}
```
- Defines the expected response shape from the `setItemValue` GraphQL mutation.

At lines 74-84:
```typescript
export interface AddDraftIssueResponse {
    addProjectV2DraftIssue: {
        projectItem: {
            id: string;
            content: {
                id: string;
                title: string;
            };
        };
    };
}
```
- Defines the expected response shape from the `createDraftIssueInProject` GraphQL mutation.

---

### Relevant snippet from `packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts`

At lines 13-47:
```typescript
export async function getProjectFields(
    octokit: Octokit,
    projectId: string,
): Promise<ProjectField[]> {
    const query = `
        query {
          node(id: "${projectId}") {
            ... on ProjectV2 {
              fields(first: 20) {
                nodes {
                  ... on ProjectV2FieldCommon {
                    id
                    name
                    dataType
                  }
                  ... on ProjectV2SingleSelectField {
                    id
                    name
                    dataType
                    options {
                      id
                      name
                    }
                  }
                }
              }
            }
          }
        }
    `;

    const response = await octokit.graphql<ProjectFieldsResponse>(query);

    return response.node.fields.nodes;
}
```
- This function fetches project fields from GitHub's GraphQL API.
- It returns an array of `ProjectField` objects, which may include fields with options (single select fields).
- Important for understanding what fields exist and their types, which relates to how values (strings or enums) are set.

---

### Summary of `IssueRequests.setItemValue` method from `packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts`

```typescript
async setItemValue(
    projectId: string,
    itemId: string,
    fieldId: string,
    valueOrOptionId: ValueOrOptionId,
): Promise<void> {
    const mutation = `
      mutation {
        updateProjectV2ItemFieldValue(
        input: {
          projectId: "${projectId}"
          itemId: "${itemId}"
          fieldId: "${fieldId}"
          value: ${valueOrOptionId}
        }) {
          projectV2Item {
            id
          }
        }
      }
  `;

    await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
}
```
- The `valueOrOptionId` parameter can be either a string or an object with an `optionId` property (an enum-like value).
- This method constructs a GraphQL mutation to update a project item field value.
- The mutation interpolates `valueOrOptionId` directly into the GraphQL query string, which implies that if it is an object, it must be serialized correctly (e.g., `{ optionId: "some-id" }`).
- This is the key place where the distinction between a string and an enum (optionId) matters.

---

These snippets provide the necessary context to understand the type and usage of `valueOrOptionId` as an enum-like object or string in the `IssueRequests` class, especially in the `setItemValue` method.

</details>




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Let's first check the ValueOrOptionId type definition
cat packages/suite-desktop-core/e2e/support/reporters/types.ts | grep -A 5 "ValueOrOptionId"

# Now let's examine how setItemValue is called in the codebase
rg "setItemValue\(" --context=10

# Let's look at the gitHubReporter.ts file since it's likely to call this method
cat packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts | grep -A 20 -B 20 "setItemValue"
```

Length of output: 6433

---

🏁 Script executed:
```
#!/bin/bash
# Let's check the resolveFieldAndValue method in gitHubReporter.ts
cat packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts | grep -A 30 "resolveFieldAndValue"

# Let's see what the GitHub GraphQL API expects for the updateProjectV2ItemFieldValue mutation
# Check for any utility functions that might help with this
grep -r "updateProjectV2ItemFieldValue" --include="*.ts" .
```

Length of output: 2808

---



</details>

@Vere-Grey I see what you mean now! Looking at your `resolveFieldAndValue` method, I can see that you're correctly formatting the enum values as GraphQL input objects:

```typescript
// For single-select (enum) fields
valueOrOptionId: `{ singleSelectOptionId: "${option.id}" }`
```

This pre-formatted string is valid for direct interpolation into the GraphQL mutation. The bot was concerned about raw strings or objects, but your implementation already handles this properly. The current code works correctly for both text and single-select field types.

---

<details>
<summary>🧬 Code Graph Analysis Results</summary>

### Relevant snippets from `packages/suite-desktop-core/e2e/support/reporters/types.ts`

At lines 86-86:
```typescript
export type ValueOrOptionId = string | { optionId: string };
```
- This type alias defines `ValueOrOptionId` as either a string or an object with an `optionId` string property.
- It is used in `IssueRequests.setItemValue` method as the type for the `valueOrOptionId` parameter.
- This clarifies that the value passed to the GraphQL mutation can be either a raw string or an enum-like object with an `optionId`.

At lines 43-48:
```typescript
export interface ProjectField {
    id: string;
    name: string;
    dataType?: string;
    options?: Array<FieldOption>;
}
```
- Defines the structure of a project field, including optional `dataType` and `options`.
- Relevant for understanding the fields that can be set on a project item.

At lines 30-36:
```typescript
export interface AddIssueToProjectResponse {
    addProjectV2ItemById: {
        item: {
            id: string;
        };
    };
}
```
- Defines the expected response shape from the `addIssueToProject` GraphQL mutation.

At lines 58-64:
```typescript
export interface UpdateProjectItemFieldResponse {
    updateProjectV2ItemFieldValue: {
        projectV2Item: {
            id: string;
        };
    };
}
```
- Defines the expected response shape from the `setItemValue` GraphQL mutation.

At lines 74-84:
```typescript
export interface AddDraftIssueResponse {
    addProjectV2DraftIssue: {
        projectItem: {
            id: string;
            content: {
                id: string;
                title: string;
            };
        };
    };
}
```
- Defines the expected response shape from the `createDraftIssueInProject` GraphQL mutation.

---

### Relevant snippet from `packages/suite-desktop-core/e2e/support/reporters/projectRequests.ts`

At lines 13-47:
```typescript
export async function getProjectFields(
    octokit: Octokit,
    projectId: string,
): Promise<ProjectField[]> {
    const query = `
        query {
          node(id: "${projectId}") {
            ... on ProjectV2 {
              fields(first: 20) {
                nodes {
                  ... on ProjectV2FieldCommon {
                    id
                    name
                    dataType
                  }
                  ... on ProjectV2SingleSelectField {
                    id
                    name
                    dataType
                    options {
                      id
                      name
                    }
                  }
                }
              }
            }
          }
        }
    `;

    const response = await octokit.graphql<ProjectFieldsResponse>(query);

    return response.node.fields.nodes;
}
```
- This function fetches project fields from GitHub's GraphQL API.
- It returns an array of `ProjectField` objects, which may include fields with options (single select fields).
- Important for understanding what fields exist and their types, which relates to how values (strings or enums) are set.

---

### Summary of `IssueRequests.setItemValue` method from `packages/suite-desktop-core/e2e/support/reporters/issueRequests.ts`

```typescript
async setItemValue(
    projectId: string,
    itemId: string,
    fieldId: string,
    valueOrOptionId: ValueOrOptionId,
): Promise<void> {
    const mutation = `
      mutation {
        updateProjectV2ItemFieldValue(
        input: {
          projectId: "${projectId}"
          itemId: "${itemId}"
          fieldId: "${fieldId}"
          value: ${valueOrOptionId}
        }) {
          projectV2Item {
            id
          }
        }
      }
  `;

    await this.octokit.graphql<UpdateProjectItemFieldResponse>(mutation);
}
```
- The `valueOrOptionId` parameter can be either a string or an object with an `optionId` property (an enum-like value).
- This method constructs a GraphQL mutation to update a project item field value.
- The mutation interpolates `valueOrOptionId` directly into the GraphQL query string, which implies that if it is an object, it must be serialized correctly (e.g., `{ optionId: "some-id" }`).
- This is the key place where the distinction between a string and an enum (optionId) matters.

---

These snippets provide the necessary context to understand the type and usage of `valueOrOptionId` as an enum-like object or string in the `IssueRequests` class, especially in the `setItemValue` method.

</details>

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-25T10:24:26Z
author_association: NONE
diff_hunk: @@ -0,0 +1,354 @@
+import { Octokit } from '@octokit/rest';
+import { Reporter, TestCase } from '@playwright/test/reporter';
+
+import { scheduleAction } from '@trezor/utils';
+
+import { TestReportProvider } from '../annotations';
+import { GitHubGraphQLClient } from './gitHubGraphQLClient';
+import { LoggingFunctions, ProjectField } from './types';
+import { BaseAnnotation, annotationsForProjectFields } from '../enums/testAnnotations';
+
+const ORGANIZATION = 'trezor';
+const ORG_ID = 'MDEyOk9yZ2FuaXphdGlvbjQxNDY0NDc=';
+const QA_TEAM_ID = 'T_kwDOAD9FD84AMZXd';
+const PROJECT_NAME = 'Test Results 35'; // TODO: Get this from CI, probably build name
+const VERBOSE = true;
+const RETRY_CONF = {
+    attempts: 3,
+    gap: 500,
+};
+
+class GitHubReporter implements Reporter, LoggingFunctions {
+    private _octokit: Octokit | null = null;
path: packages/suite-desktop-core/e2e/support/reporters/gitHubReporter.ts
user: login: Vere-Grey
body: I have change first import to be just of type
created_at: 2025-04-25T19:29:43Z
author_association: CONTRIBUTOR

PR comments:
user: login: trezor-ci
created_at: 2025-04-23T21:21:30Z
body: ✅ Previously successful run of **[Test] PR Suite Web e2e tests** workflow has been found.
⏭️ Skipping tests for this run.
💡 If you are unsure about your latest changes, please rerun the workflow manually. (Use the Re-run all jobs option)
author_association: CONTRIBUTOR
user: login: coderabbitai[bot]
created_at: 2025-04-23T21:56:51Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This change introduces a comprehensive GitHub Test Reporter framework for Playwright end-to-end testing within the codebase. The update includes new documentation detailing the framework's purpose, usage, and extensibility. Core components added are a set of TypeScript classes and utilities for annotating tests with structured metadata, extracting and formatting this metadata, and integrating test results with GitHub Projects via the GitHub API.

Key elements include:  
- New enums, interfaces, and constants for standardized test annotation types, categories, priorities, statuses, and device models.  
- A `TestReportProvider` class for centralized metadata extraction from Playwright test cases.  
- A custom Playwright reporter (`GitHubReporter`) that, during test runs, creates or updates a GitHub Project board, adding draft issues for each test case with detailed metadata and results.  
- Supporting classes (`GitHubProject`, `IssueRequests`, `ProjectRequests`) for interacting with GitHub's REST and GraphQL APIs, handling project and issue creation, field management, and value updates, all with retry logic and detailed logging.  
- Updates to the example environment configuration to support a GitHub token for local reporter runs.  
- New npm scripts to run Playwright tests with the GitHub reporter and a new dependency on `@octokit/rest`.  
- Test fixtures now automatically annotate tests with device model information.

No changes were made to existing exported code entities outside the new reporter framework.

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNwSPbABsvkCiQBHbGlcSHFcLzpIACIAORIAd0gAcXUACWwBPxJufAoaPgAzPMgBfFxYSGZMbDRfTHo0PBZqKJpEUIpsDERo0vlsREpIADVKEjBk/3l4DFwKRWwGaXRs3PzZojD8fF8lRHgicnoCP27IL3wGOq95CXg0MNgPJicMWmQAA1lnLHbOEgAJhIHASJAEHH861wn34fG+vzCIQ4QJB+wA1gRuJCcnkYRoYAhkARdtkgvB/MhHqlcBksgR0WQFBhCodsP56LNVp8NGQJLDWZECQBJUIMfytZAlS7XdroLAkAAeSHEGC2AEUAIJItDMHgLERifhYGl0gA0kEGm1WDHZ/jmt0gsGctCYSnovHwhtCGF1JAJ7n1XtEoVZJC8H3Q/lKaCG9HwWEwGHK1HgCcgSlZGHUaZ6kEKCz1FQ8UPwBwILgDzzWeOGpHIVDltCohVCSAcKwS6gQf2rnu9+ZKJDQDEq/wtTCkFGtZQqVRqdXljWa1RoJxCZx6IrbGAYXmw+2y83klyI8AYg6K2ki9H8QRCMjwGfwK2ToVkJDFzwY6JQhVW/w1vk6BeBKtDyEqKqIFWHjkIqoTcF4SZRB0OTILMe4Hh4Eh1PAtCpumXZzo8NC6lUJDMAIwy+mw9AAPLvFQFpoLQtDWrQVyOOwBF/PgTweNw3i+HewQdMx7z2JkQz3nMkAJHkv6EQphSXAk0GEh4tgZtI4rwNw4jpnUiB8Z69yHl4szosSfGAf2IaQPciRSia6SZNYBohlSEmnIBlLeLgzmQAAwna7DIMUfCzpUADK2DqB4YJZA02mIJi+DcEiHTQeYlhBSwbBzMgDhOC4bjVq8BUBca/GeD4ZKiWKCYHB0+rwNU06Ovg/5NAQq5RGwiCIGgpDhYWNVMHMszYPggwoHMJBEI2ub2LIqHMBa8wkAAXnkYAMPAMFDORg3DSssxsbKHgVNQ+okPcM2II6DgMMsg2FN4m5Sv+xYxAA2tAIQALrWHYsXxZAADq4KQKimUBX0yXRP9QMg5AYM0JAAAi0hpRlsP/L0cnKapyBgtGxTdLQFp+V4qpbFyP22hQ9qhL53QyCQ1qpXp3B0DBx1DSN6C0PcR3XW233PNGqCYAMu6ULg2hzPIKgzaz1ZIYBo6YCNG3VvJFDoip+BqYwmClCWlDnNUGC1D4AwHGqNXRDYExdImdVCIIhPpQZGAEpqrE5gmNyyHr4woFSpTTiQ/4Vew+ZjT9gzDGM/iTNMGYUiGUQ3Zg5RS5aQwUBaAAC0VXIyuDRaI7LqGH9UhNapyHMm0aPIgvP7ayF4YNwercCO6KnQ53gNio8AWbg8j+JrbR8Z8iFoLICTTkQsC4HteQkKXACMGgAKyAhoAAMnwWm+zfOurqCDz+I+oG+RdRGx/hiI6DN9gstBLJokCxHxd6+RC5sUQLaQauYLSFBIHQAQQ8LQlHvB0XMpMI6mTwlET+Lx8phQ0EYfQxhwBQDIPGbqzRiBkGUGuBQrB2BcF4PwYQIZJArDkAoJQVBVDqC0DoAhJgoBwFQDLRM5D6xUKiPHOYXAqBJGKu1eQbC3TKC4ZobQugwCGEIaYAwHEGCIAAPQE30aiMAS8V5rw3sQbsmQwClnyJQDQzBaAcAMNENxBgLCQE1MKCh49qFyOcPILqjBnRqmkEYbU5Aki6K4nMHi+ZJ4eFmPMRYr0aqmjcgDFqrtoSUGYpabMYZ6AFj9AbX8EVny2kqtaZKdi6alALjgXqrRGgSRtnbOGpNuxckeKWcseRZDbmfkVeYv92RtA3EmFMftkAUSoqxTBvZsFKG2GbfS4y4bkUVvhRWklRzoGJBua4Yt1CRAtLwcY5JyzSAtKhbgiAJytCIAM85048j1w2sOPUHQJTrSXNpe4ywqiKHDASTGdcnaz2HEdAQcUIwPJqnU4YPUWjiGuPbRgEo5TUlclkOyRoyguh4OlbwLS5LdgzC2Nsg1RJrEpOwa0w59njjmphNiTs2DbOoI8ZKvlpD+QRacQoI5J7qFaJALU2RFrSAOEpQ2xs1J4IMIGEpbAynMmalVYJFR/AeGqFyV4uRyCFS4IBKZisZnaSzM3DcnKWLcvyVk3AOS8RWAWGZYY4sYbwSoGILyxS8irmJESLZ9rFbiRONWDJWQXX2L4F68Uw5sUSVyIJOeyBo1Bm9F0uc7ZaX+BUswp2gE1avBWMlJQitJ7qUDJm/FoRCUUE5Lufch4wzwoUMzaQRr2VbB8raz8YaO5LEqLGewFrBi3M2rqT5LV3ZhFkLzV5aZpzTwtEoQFeqQVeAjTQyq2UDAAFVBZJJ6KMsQKCFBTidCbVZLFWlvh4tZTp5LiIwEXSQaKul9Let1IhEgFo+QUgTJVByzgHgCEiMyVkRB2TxIqZqKwwp0AvRlf8+tkAaKbF3RVBo4USju2zMW6sSK+AylDpeMICxMiREQLAHYdN+ZtX/ZVeJGFW3lqPC4cdjYFryAQ0hs2Pgc2VGwBlHysBdXoFwDQZg+l/WrAPifE+zBkBKCQgoz8YImT+HmPAaQ/MmYsxQHJyIrG/YXCHmpugYmLKymWqec8lHAJ5oM0YZIcV8Ly0o0qGg7wr4eFVYkBSkcgweqptpKtFknb0aSKce9mHEjykfRZ6evMZADG4DsxlttVPiRS8tTMsxg49GYszZeyAJopJ8DUjABX0yDFOru5gih4CFFkAFmAIRY24DdfgD1fARxMCbZsW4SqcpeNplQy9EmPBKD3M4J9/B/xKlyfGPgglINOYZeIcJBgoAAPIJAAAFD9NihRuqsSjlEypsSLXLXauiDiCQsCCgSt2NW7C5uiCQktJqABKPBbjoj4O0XfYeI19EOHimADEWIt7+H0byRUf6hR8hccDjxlhvG+PEfQAJ3HgnazCYgMqHgeRKlR/6PkApElyTHWJnZ89haNES0kX2uZFxAYWBgUDOFpwqCg0MC96YKk4tpG5BkZACTRS7m188od8mSLXGAT7S9lj0YjMMCpnxkjCmgGkQ9AAhAA+tAOiABpAAorEWECQx3JO/ksOg66dLTnmQcgC+BGSvZKJmnrwxyPCXZs/eMGBHQVP+J10jKAArhkKASABISdadgjtUFZpxILIKdvziDtG4T8QpClb9MzxtY8mwUJbs3tILb+3mYJq3azrc8Fti8O39Ok4MId/0rj3H7bB0PU6Bjoc0FhzjeHw2SDGOBFDsT0J9HmqfRoAKGP3GeJx2I3j+PHDyOW8nknZP99C0d6kjwjw8CiunvmboIusCfAiquAAMiqWEQrA03R+TU8rq05qnFy1RDkC4KCfmT4RNVoJ1TUOraZXMAUG/CzVAAtEcCsZneKXjfga9G4UNHZR4RkVaC0VcUcALPUIiSoC5IrY4ZLaA9MCaGDODGZHDBMNiP2CjB/agepHCfcctZACyLKAvT/NUDmCKS2XAdkIjLYc2ZwKgIJb6SZKA+7JqAONnRgJCQaSAT4J1HrPrAbWEGWBZE4PiMga4e5UlDGXzX1CzZKIbNDbVAdLlXZAsFgVYKwDTVeQ4DeNQp1IKWMEgHQs9TAZYIZDobeLyerPMHpecDKXAqINhRfVLD9V5frDBazYVfyMDTg/DPgZgdsa0O1bA3dRvIYZAZgfyPSKDUgGTSgDIzZT0XmDYFYZ6UdL4f4LwoYc+NQjoagQYNoxefwESJAeKRAbou5QYi0UAp5AZbo3gFdeuIY6dZgbo/4Gwbobojdc8EgAAWW3W6OVzaNqTQCSFiJQRAI6JEMGMgHKIKEi0oCyPIEORahONmjgTjGqh+hLTwDLQtHYwPC5nRB5gmSykMxUPQhbSwkqxYEEmoRqIVg7yAMZDUKQHWIXC8FhGOyeKiHTEViIEQH+1GLKHAmxjAWnH0hgJO030lFWEe2eywAJ3kCUF5n8ydnTHaUXBKBRT6nXA6BxP+R6ODDEBGDqFEhRN01EOQFwLAA4OCB4G0AoCqPrVsXDDJUOIB0UOeC8FqK2Xo3oE+AuMoCN1kAtxIFkFhHAiw3RUdDNODTP24BqIFwxh1MinkH4K2FwItA4LwlTGIw8FwPQCICVhakeFZEVCiB4NCF5UkxNlqxhmZhKDazmjdNoAJDoinBuHDiPxWGWDmCoAsi2jPwkg6PeBdHgBzM2VyO5SdAaGi3EIkjqWtEcwYDL3Xymzr2fTOx+0W0tQb0VDWwL02zsxhkml207ygAADE4DloDgjhOjoxGcWkuBQCsUSAIC5CeJjtSzFYuAnV1jB1sDhQ+48B/tYRAAkwjULAJoCXNCNXO3O5Q3JCC3LsLQF3IhP+xvI6Gxii0QEgNCN+kBk+H23/iS3el3AsL0LnNYNwGfw6GO2GK4CdO/OfJ402BROSUoF9F8AvynlkH+z/PiCSAW1UPvToDnI0NxHyC0IwQoDt27BcUgF0GCianPQrDnImh+V/jyFO2RC6w6BaJIAPIMBoqgDdUkHFU5U1LnKmJwjtM/E/KoIwFOw/Rgr0zVFdxSNpn5M4IUunDVHgqdN/P4usBoyc3tMQDnOaO8NmM6JGLUIuT6OuUss+GGO2PGJcEmLeVXSNNGJ+S+QWO62WNGNWOWE2PU22JwTmG6JkWkvkJ6G6IRKRO6LxNkAJJLxgNGPrTUsFL4toqsAMovBEsUDnPtL1INNkGO1wI0s2G0sUqIF/Mx37zACMHByH1nxhzh3SgRyn1RFn2tLxGMVywMX+AiqXxX17xB3Xx8TJP8R30CT32JxGkP3UJCAGr9mgA/VhDIEcHp2QBnOoVOBmqumrCdLSNpWCXmq4rMsTkcM+AAHJ/hjkSBLrX8F5rqQhuL7rRi6JEBES9NFQBQxorqywTdVxpxFR7rVk/qPrqAgbXr/lTh7Q/Q1DsZN1PqgaHr4a7o1jArwxKK5wfoJTydLr/KNjt17qQDPCnKjT+z1r7dZl4ISEogSDVgbtca1CAFcAvCaBnlpwczaAeQvElDmLFZZITretXL648pLgZS7cx0CL9CIiPCQhBL3lp5YRcbn0mBxaEKBCSaQhoo5jVrcsNrvU/N3RX1R0lCmbPgWbsYsw6AebIkks1q9QhadaLLJbgSUkf5lh6B6bijaZSiPAHjkAVa6VpAGVi0NxPRpV8Lqz+VaYEU6gEwth6bHhhtKQe1rQnaJ1EAxa8gzjqhrTm4+I1ac7FCbsHbUbEatiDbpagDs8tgCbgV1MUAlBJowwZTWVW1IzPhD0MB0RkwXtlaBSe8DA7akhkKKBhUgVPgjdvDFqSSqbf8ndPbVlyC9r/aGKNlgklSsBBBs1PiQTe0ElwxIwKkaIANIAXTDqrp4j+BiSQ5fAmaOcmoEFb7UKFB1a87uBNgEVH7X6t6F0l1/lzYf7FwwL6kVJholV1iSj/0+b6LMAtVvoP01Dp6hhZ6Ew7cI4V6A0+Bc8HpKDIqioR1PdPhTLUHly/YXLLk4pbK0GMBZi0JaHzLTjGGPK5iWG1DFjuh2Gxj2aBluGpjFbZBuGCaMavB2HkpQCQrcBGHDpsE/DCp7A5d2t8H4k1zHgviD7cDBUP0EUH6X646JIi7Mi0B87NayopMbtJCKs5II5q7ThFoZoMot6Msi4R4qtfM5znHhy8gjdFAEq3c9IKGTsqslZrRrLAhqGBj/lhiuTPgvG8g+tvRhz9N4UUSQnisnYA6p1flZ1Ohuhl1BHXdN0G7wwGDaE5guSJHnHA53RoB8AnU0mEwq0xDNlbrXkqH+j2gp00JsmvlHleGXBd0BG3LAcjAJtNQmyq8bJqx5tfsltOzuySheztsByO98FIArd9ahbaHlreZPguBdqtTSbWjL7zrHanqOhbqQbTgrrSG7rPgABuNMrU96pG+Ab605hw85/6wG9565x6ssN54Gx57IU+rUhG9GiG95lGz4CFgKyu+m82/GtG+F9TYmv8zZ9aoWtmhaCYrgaui28oHFjmosqIAAXkgEupZuCictJdoHRagBqYkTgcFqdQVrcuzplK4FdmG1oGADZZFrXQ1qID0B5Axa2adR1t+X2ZZyiEJdwCttmHJcpepcVeOAZa8T0IpsdslczqxsqBVqIsUHwG6N3L60jrssxgTBICsFjEteteHOvG6M1GaFtcGmdeaEdcnm6Oil+OtJtrwUZa1f5vgblo6GdtOM5eMsgB5byD5d1Ysuyc2FFcDc1eNrLthZRcJqCpNsvujfUN3iN13m6OgEBGgGLdGNLaLZLYAGZy3uju7e6TY6HU2mXm0CgJ7ycUGSBGHc320Pg5zT7ujcDujcbdmfDn7mDfBPhca6J9G2jgHp2f6s7dgc6F2X7Fw4nyHcxx312p21DyA6BEBvGKBVw6YxWg3jaQ2FHtUkGp6Z7t30GTLnqH3QjKGbKBjuHhiv3M6f3fluHOGMBuHLoOahHH26GUrBWwO32/Ks3RHgOpGZG/y22owpDkAHGxNatQj834mKBfH8SAmX7ujcPEmQxkmj67Lqm9C6mGmgc+8IA6qDAGrIcR8JgWruA2rp8p8HAur8h9FSMDEzwJcBAA8KBl9jLhry8N9KEt9JISppC0zO9BFkAbs3sTMWMwonCXCLFWYNwY8wW1Do1RPX9r5f8FpeM7jOgY6qp6bM1SO/UHIHgUhcVLrkAbArdopoB/kpgTHYB1RH8vEkN1JRQ5ocxcIczkA6IxBvd1B/lVT1S8LCiTtPhhQaUSBXYkEApYQJHo17OYR/sDlZBdxwzkxBhOo/g9PzgOjgJjtPgEwjcFpZhPguTTzrRpR8AWIalnPhOs17Jkp1BwoUmPh+YY95grNCviuec8HM10ppsmpVkyAHBoxnDl5XD15Qh7cBvnNngi9DVIgLMqIhCElsw6NOs2Z/ZD8Y99U4lZgvIzOKBX7itxBwv4kTi9URwewPBavYg6JoATdPPNQbBoArdMZTXYgTdbA6Jkh3Popopuigo6J1irBH8rdgfQfRjhzvEUfQeCvThQlaAoNYwivRwpvZowIlWo6PRygGUQHrxxkEUk6NTFBVlNvQwYzsxnvsz4k9vPxlpDvt4gxXpHY+0HxEyFQPvWnwT9vlpauEwnVNnuauTYvWeqinuHgue/Zw4sBTzONmw0BWxuu6RI4Oxg6hhJpPTWnvDc3K1rx+orzFYhldMY4pBkAMN+3kBjtrhRwogKlY4e59NdxML/ktqVgfpXNXPeujR3fc3/gwBO5RB5cLwVbbk588QZxGkaB4JonRsJghhIgo+hv/6DNJtfBM1ENkMLSowEoqB/WvaKUneTx8AzwLxkowFngf4bwUNLU8eKyroqAegA/Qw6fKQlVNR1YPASE99XjKusBZeMAFfmvPiNvtAqoxc6p6SD6ifJuExpvcUb65u8wdqpfPwz7TwzxMmlgheC9hVJ5xkRuSLLj0FDwpwygjoz/at6BKAFg+AaziNqN15PsW3CyNAgYCyA9w/oSACFwKIrBxK4qeLsMFyrH0Sg/bbIMZH3Ca99+deKjEPGwz/I1eL3CzIgGJ4lciy8STPD0A2QLBYU/pIgdv1K5glMypIBVP8i/4lB8elZJVI2Urzd9pm32WvPMxWxdkm8PZbKv2XCBrNkOwbIEoZ1xTGd1OZmUOlsE+ByCJGj+JvufyICjkgKKCXwlZUHwsc4oo+djpxw6o8d58AnfREJzpCidxOv5KAFbiEH2J6AmYJoLTBkHCc5BiXM5noPvgGDmq4+VqpPi46dVzBD/SopYNkFhCxOWXSTrVXqr6DpATVIwQEI45BDTBqfPjhYLSwGYhqmOUarjlk40lpqoSWasqhDSqc6cK9ZAOOy/REkdwHbEcCsChD0pzeWwXBrNGwI8YxklIbzjXz84BdU6TUK+ullDxzQaorHMfKlAnwC8SEYAAgGAEn5R4L+vHKzrkhlJDIT8HtTjJ8DUFEANBWg2/GcTHqdtFGCfdrDkU/Cakqi7/J2GSV8ADQT0CKVgTKV3RIQqInfQYT0GL6BhPgeXXwg0KBQtZ1MMgWME5gwxsF3ceAeokQwdwRZwgZyf5P/gcRow7oygXwMcMaFgku0ncRgqsh87cB+hVQPAPEmSj3huMnwpLrPDJSnAMMNsU6JVD3psprQrXJ2PWgRRkiOsrIjyH6mYhBwnYrmZ9GyN3RbUwm3ItsLJkPrwoA4QcfdhiLSQr1xh69aMMEjd5DcFMxYIvMuxT4rDrQtxahMmEPDJRF2GYMstkJrTVg1ORkPiBo1D46NKMFIzsM8G15YprQ0fSwnyLrpUpjeokR5Osk0pbA9RzLeaLJBt7VoCQyTVCrcHyTdBlo2QtQmlRIB0QKAc7P2MKG5qhZZmzgZnM0JDqyROYxYQbMK1Oaslt6TCI0OkxaZAMX6kAzGLI3u6dswSPgZhFIEdBP816POPtIuldHs9K8hw3NnKSpHUJ8RhI8vgphuFbB0KOYTsN0iWRwxXRVANVApAbLY5JmPAmqJmObJ75G8TgkQa3jEFTihyabRZACPJy7D9hY5JqPqyZ4Ds1Cp4boqeCtzRkKKu6T4KeFdg4ivhugxeAkOHyGC2OKQkwTPjMHdUshOjWwZIONpyjycfw/sQsFqKDk5yeEBYqcgnbckkRFFMYd+N8GJCJhxgtIUBIyG4B+OUQvqmBJiGXtjxlAE4Z8CCgLk8uUDSKrCBzFm96kw4/zkSMiqoCjUR0CpCyPEKR8dwPgiHDhL/GTDcYgE7joROIlrDSJ6WcCRRPbZUTGhahPLuqGCAuB3x3Enwqb0UEpA+h7EjkVxKGGUZoEuAQglyN5JVQuQWE4Sb+P8FTDAh28YIcBMyEkTDEZEwYhBMonj1lJnwGpqlw7B1M8umkoYUxJyAtDWJ+kgLsUU4kOjKM96SMq5jvQCTMJzHESfZPEn4TJJKw6SbWBlLuS5J5Eo8YpJ8mT1yOEYFMXPQpRQk6i+bJCWhL9Bfi0pdk5IQ5NSFOT0hOU0CYVM8kKT6xvkvLuVPTH00apCE+EtzVGJDtJ2nOadtgV3a7pjRdXedqh2Xh75PgQ0yqeg1Sk/ikh/4tqRJJCEgS3JZo+ScVP6mT1BpaokKZ+J0mtC9JvndiXFIqSqiKO207CS1L2mZSOpBErqcdI8l2CzpUEtQoeiyytA8uooCiENOuknNmJuktidFOJEWYnpJQEUU7EeAYZ4oeoFAdZOam7SxJ0wxHJ1NCEySCpOQ3qYDPmilTyctEpNCQChndpQpt0yKQ9IRmxSGZXwyjHxNWAvSIwb02yXjLwnfTspxMvKbJLJkAyUOQMvyaxExhUoApwQaGdpNhl3T4ZHE+JMjMGwejVgevA3klPiwpScZO03CQBKymHTXJJMk6UVJQ5xjcIY6T4AmKTGbSMAaY7Ll8AOoAAfSAAAG8b6qY5xEWIAC+TUo2aJMFmEyfpIsuNGLOggxCaqDHeIe9IFkmyhZZsoif8AcFG0Pp3bZJvBDv65C182OMajJzJRFCicJQvbIGF2rqc8QXwc8jJV3beDp+/pcDrMn1rujIwjwG7GanA5zQDgGeasCQ38A7QKAmzCQDXBELcBXZxA0nuFAvH+wvErYQPOoLO4bhquoQUMV4ARSxdBIdGctImB7k717INzf4LuWKBaAW5V414kgyGCswF4dcyKuOw0Bwts2mNaGjM0I6EDPwqyH6BRBMIlAgR4YU4d3H0zNo1Cv8zWHkGdr5A8oLIRxFsUTx8QC4S85vhOCaYLBfACqBBHwGeHll3glZULECPlx8wxmUnVcTNl4E145mHZQQYsw2yiD28e2bvHRxBxxCmOIcjKQTKnxpSNAQgYyBgFXwjVC5BQkuZNUJxxxy5SneSEoT7jfIS8qCduFq2rz2U5FsIYXH7GfYdAUQwIUEOCBxDQhsuEkEhsiFRAcB2OeivEDzV8ZzhCSgTZAPOhW7mI3CunXgpHn1jQwK0KQgSXYu7TNjwwDcJOomBlEzS1CEAUjLCB/pAK2snI/ieAl6iG83IMeGxT+nIzUIboPIZyVJIsFWDMgNgrLlWFQBkBBcofPTtZ2fRUQgI1CcItGjj5KMnMYFUfkErvpRiaonwUuFcAZDqBiJHQWEGlIciVFlonwAAHqAh94oy12rKxlo/RPgG6bGBv0MId4eaynKUthNCw/Quo/vRcP7g85edy+yhQfpRgABSaAHCLUMCYWgLIjID+MGONqnAXJ4/JQpmhjyAVb8uEaeEqiTxIK+Au1Z9Ekq1R8A6StNXcDCTJjvclAy4ivAfxbLvz+B1C71LQpbx9kGFh462VIpuwyL7AcivmadB4V8LYQlMYYMorqGDFqKtFT4NEH+BaKQQiUcxfkGiBcBogPwB7pAEVASBCgAgMANUFkDlKIAPAbTk4s2QQAaChwMlhoGCFmI1uG8DQEKqIDid1E9aMlolHUSkYRVGS36STOyUicoh4naILpVJXkrjF2isxXUjpUxBGVWAFlWyo5XLxuVUAcVTpwFVbwWQwq0VR1TtVOKpVCYGDLKogDyr2OSqqISqqJlHT1VkQtYdqt/JeTWcN2DdNpDmWB8sVpAHFU+wypQAyVbS6Lr8SImUhcAJq6IMMtGUaBd4Oq2IfHLYWJzjZ+002XctylRyIhwnPLuJwEVSci5fiFCKIoU7fKIkfNaQbrlxQwShEjArYTLTpGkB4leKMUQ0hdAiZwi8fYBeiQoC+lswW0Ekd5C+RFxOsUXDpaEBh5ec9w+yywvdIJHsS91CcVeCY3VK1dgpETB8M10CKETgSYXDXnzwgjdhtc50SMlnnqQYY2Ep9AvFzM7lJZrWsEugLHCVbFI1RgDFuYCTta4KCeMA6npNEXA0E7Mq/MaIPCzKRBfAQUZDGUgVQuN04HI7sfGi/iWT0Ai8r5S6MMh5lwwRaPtNWF2D7BQgBBHsFsHrRKpK50g6AXd0+D1T7STwG6DqhNh3ccFsZH6BjJU7lBQunPOliASe6whEBcIb3j8nJIYYne+mDgn+so0YBzly8iyRaxYFPjzR5OLMLQAcEqhNgMEhTWSJhJsixhayhdZgFIEYDBQBQFxhhlPqu4aADnH+IhAVxdN/kp4UmL8C/ooZeWo2QZHNR16WbLhzPHXtdiSwYZ8VhY2dUQvWyLqnNy0XlH034gKgKBPQsTROuj7Mb1x0gbOA+hkrBdQgIfS0lalcGhBogEbQYH0BQHLs/wt0FiZptpkqcAKEG+milqKSnMzR/yLUVxmiWYClsjPaVXBkgzCFuMuQOzBFrKHk4Q+l0o+k7Nzoxb6A1W32ZejFwKhlQtdFKf2x017CuYl/awleFv7+AA4dUXZWPSQKXosxckGvrzDr5zgG+FwJvk5mSg4L2BuA+mhvODK6atgkeX1MODUAYVwVEzbgeQpK0wrL0CzYQUs3oWrM9sfUrwb2vrVijg55a0OcnPDnCzg1osutXSAbVZcTaJKqADApYrRcKAc5Y7O0pi6cBIAm6pnSdtIB07IAZ4zYAcJmS8U9KyQT8AUEQn+ydKKayAHeSuEkq9KPGjnsdngp9YsiQwYABIHwB4RRWGVGXSZrM2108u8urgIrqQAkBgAPsvCGVTVBPN4RIIQOZAE9m2wfAGumijLqi1ijjs+wMrUNOjaahv8wAbtrQz0AK7CwRulXWrtoCO6ndqalbWKI2n6Njs/bNMebqICu4DgHIIaVwD93gdA9LAYParvV3VV6OA+XHRwsckE7U5Na1zfolcwZcGoMciTnkKEXjU218nYoSnk7wj130vMU5T+gx3yz0u16rKDoTdqL0UClMpArmzs5ijA6TnVWVaAt6s7M1ey9gEMjbFFFNtz6BLObE/XWg9ZfEdGWKOpifhneAkQrWqItAh9uZE6zGYAEwCQbkfUvo4YFy2sr0UlJILhE2NGkZQjBv5pdBadoWnIFVHNifB596gXwvmSBQCYK++UGMfZgTAgF70veoKdjuvH2ASEUcVWTFLIG77WI8ob0R4DYRbcDRHgYUJjGSn9aQFKU2pJ+BFI1QbsGMiUcQZALlFVtqTZA+pgWjkl4sWAZjMIPgFFA55qyUyfsh5nDcP99lT8BDOYAJj5Nm2y0KDOxSRKe4ko0hBftI2Yzc2jwM8FICwAP0+AES4g45x5SHrCRGBv2CAR16yz9euAXvWayQMKa4tT+yw7gbs0kaBwjPIaBzzpZhAUJ/yeKgfpEIPdOsOvD0JfvoO1jeasAjGLweQOfAdIJjEgPDKlaIVQsdjAwmAjiMxgfwj0WMM8AUwcQaMHgIINT2BLCtNqzxLkOgcRlHES+Rh9iSYce3Rhv9v8XOIclMzionSImGGJRDoDG10NfoVzbugdFUhow2Qt7ZUG4M7ioJz6RbhsjjFDRTJi28ZmQvm7V4NxAguFUjroV7ikV6zFDj3rS7V6b1Ca9Ka1K+ml7q1Fgqvf3oCi2CY+Fo3YKpDCaiDEBEnZ3fRR/0VgGdGa9QFwGAO4B+dpK+A2l0QOWTjs9aBPcK0+JpcAESgcE06Sz1K7jdTpcPamqYPR61RoJsUbCcqrwng9zB2gN+WRPtFxDsmBMRicslYn/RS/CiBSdwHx7RdlVV0oPUdkv1wTDs5MSydoA4nlduesPbqtTXmG5ZaXGwyCbBP0nKTXhiINbqdIWh4qierk4icqoa645he/mRWtOPtUI5RO2tfWgOMAl85girxC2rxxydd8Zc1vV2puxAjvASSIfafhlr3ajQtnPfnlw955cRggIArvcBIhRo9+qs3ZbPq2C/Ghk24gA4mFoEk8d+s0Z5RZm1Kfg8TZxL1GptRG36O0m9BQ+Qe/UzAqoxBxkR3SdgVRRcEG1vjnzj40aC+r0+mhqN0P6N2N1YK04T03l8Qqh3IK9ZlzOJeCvUhhExg4HTQCT5Sc8eMLUQm0UpAd9AccYZjeOsVBsqGeTDgd+O9yBaQKA9WfwcSH4vBK+mqMUB8ARk0Z4Zkrng2eMuI/y85WmX8IOYLl4tSQOtBOqS2rAyD6JF7MilXVkRGemhpkB5qPCiFOstBidQwePMu7LJQ0mVveijgoD9Z9abUdCHYhtZoExmMC3aI0ZcwSzefeyNH3prGiULYgIpTtqfqfmAjFvII0oZrGptYzwtQC4WCTHpbl1FDaRIfvU2cYQye+WzV81WBvnt6DmpdfBkngFBoi8gdzX6D8NfmLJA4NRiRdROAW1RMrQQzkaUNVENDLCLABhn0P4HKZkY+QGaU6z5l8IlwI7KRfjOwFtBsB48yHxj0zIZWUIJCGkkwKta0zwvSIKWfz6hhC+jPG7DfL3zLslUVuCXnYcnMOcfo56uCbaQ8CqzDJ/8yo+mGlPepa4coWLl6ZZ1fHcAN+tQotF84BBkS1414U31d4LB9NlBns9cY/1KJLwQaHC3mAqR2Xc+ZZpy69K5B1H5uY6Q9u6H5jPG8LWAKFDhFkjEGpQ/yssjTv8NcEgwCJp4b6C2y7np5kZ0o9iqMBJ5VsdmWLr9orLb70IpmCiOwDoBPNnho0RwvOcr41Fho4qX0P4dDgTnGBGC1SKFmU1OKIo9uJtBaD63XrEN9QPczPKEybyAtwO6TOfUNLjo0IUOpY4fwoWrHYVIZ+dQipWbiC0dZ0kG40HDPX5DLd+cS0k3ROM7M1PxhKwkW9A0mtKBuoPcrrxMEmcdqpvHZWpTnnG3JOpq47XovZQ3HB1CDHa2Zr2E3GqapzhaqsjkV6KbbZm45WbuPbmuwrIp42vsp0nlfLHxlG98fitbr2dlALgNzrVC86UE/x1NVvwvAAXvQDOx8xQCxtJ6dQzAbWxjZDCxA/QcpnG9nuV1Im+TahFW5ilPNomj6ZJzG2KdpNDcjbbARPVAiG7jt3bpVxAAAH4uAPs3GonqeZGNE9kAAOXBVNsIngAFt8XXE1huI2yOFFjixloTAM6U71F3MN7frSu2pT2JqO7ibFEE2471txO2IE90O2Qw2t+U8AHxuAxCT8d4nrIaZymWUEceobvrZ9sB3L6wdt+nkDDsR3AYNdnk0qYL2MdflBiEMqrjwBM5aAY+ONftESEBA0A89wFYvZjnwQm1+Qxvdvmb1mmD8S2tMizkvOxq17EETMg3FaXi2s1IQNokouXur33g8ynIfBFpxQYU42DGuvUjViz2JUmofs2SgBVP2gVxfD5QWIbqJ8pmNURizGT+UcIbG0YdPEPUWMw7ljgNtspuMR07jkdWx1HZ3iYUlqNEWiYhBJE3qiJi5dNqRtIn2ImmpqiiEFJwjUCqJeERD/hHunUAm48IiAE3P4EchghaAJuNeeok0RsOa2AAFl3hURVAaAUQKxAACcAAdjQByOAAbLvDEe0ARlcjmtiQAPg1sT4tAOR4CAUcHxFHujtAAAA4LwfCIhOgAECGPAQFj1iMpkKAqO4EtAXeDW1oA1sVHgIE+E46UAnwSANbQEDWwse7xd4cj3eAY5PhiPhHBgNhwo5UcqPCgu8BRxY7QAKOFHAgBR4CFUCqPgnaANANE7gSpOYEscCx+E4UeFAT4gIYp/E7YfAg0ASmXeAfCUBpOa2R8AQLvH14KOxHQTyJ4UCPjyO2nFjnx4CFMc1tHgNjnlVY6MdyOWIB8CRwo5IAqOonAgcJ4UFoAWPwQXjhgGI/6cqOxHujo57vHwgMAsgMzqAEU48cCAa2Pjkxwo4YAnwGAWjhgBE5raKOWIJ8SJ/0/scMAVHroOR+E+sesPbHhQcJ8U7QCAgGANTk+EU8mfpOxHL0OBGc9ydTOVHqzvJ544YA1t3n8ThJ7Y4qgcOuHPDtGokDoAm5J++gIAA=== -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=trezor/trezor-suite&utm_content=18505):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: trezor-ci
created_at: 2025-04-25T10:33:37Z
body: ✅ Previously successful run of **[Test] PR Suite Desktop e2e tests** workflow has been found.
⏭️ Skipping tests for this run.
💡 If you are unsure about your latest changes, please rerun the workflow manually. (Use the Re-run all jobs option)
author_association: CONTRIBUTOR
user: login: socket-security[bot]
created_at: 2025-04-25T10:36:41Z
body: **All alerts resolved.** Learn more about [Socket for GitHub](https://socket.dev?utm_medium=gh).

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

**Ignoring alerts on:**

- `@octokit/request@9.2.3`

[View full report](https://socket.dev/dashboard/org/trezor/diff/fff49f17-d93a-4cb7-907c-43102aee01b7/f4d3461d-1629-4a8f-94cb-ac39491e9b04?tab=alerts&action=error%2Cwarn)
author_association: NONE
user: login: Vere-Grey
created_at: 2025-04-25T19:07:03Z
body: @SocketSecurity ignore npm/@octokit/request@9.2.3

used in reporter, not product itself
author_association: CONTRIBUTOR