playwright-slack-report-burak 3.0.112 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,24 +8,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.generateAllRunSuites =
9
9
  void 0;
10
10
 
11
- // Helper function to get build URL and CI platform name
12
- const getBuildInfo = () => {
13
- if (process.env.GITHUB_ACTIONS || process.env.SAFETYWINGTEST_GITHUB_TOKEN) {
14
- const repo = process.env.GITHUB_REPOSITORY;
15
- const runId = process.env.GITHUB_RUN_ID;
16
- const serverUrl = process.env.GITHUB_SERVER_URL || 'https://github.com';
17
- const buildUrl = `${serverUrl}/${repo}/actions/runs/${runId}`;
18
- return {
19
- url: buildUrl,
20
- platform: 'GitHub Actions'
21
- };
22
- }
23
- return {
24
- url: '#',
25
- platform: 'CI'
26
- };
27
- };
28
-
29
11
  const generateAllRunSuites = async (summaryResults) => {
30
12
  const suitesResults = [];
31
13
  const allSuites = [];
@@ -273,13 +255,12 @@ const generateProblemCaseList = async (summaryResults) => {
273
255
  let text = `${title}${list.map((value, index) => `*${index + 1}.* ${value}`).join('\n')}`;
274
256
  if (text.length > 2700) {
275
257
  text = text.substring(0, 2700) + '...';
276
- const buildInfo = getBuildInfo();
277
258
  array.push({
278
259
  type: 'section',
279
260
  text: {
280
261
  type: 'mrkdwn',
281
- text: text + '\n\n\n' + `⚠️ *There are too many items to display here. ` +
282
- `You can view more on ${buildInfo.platform}* ⚠️`,
262
+ text: text + '\n\n\n' + '⚠️ *There are too many items to display here. ' +
263
+ 'You can view more on Github Actions* ⚠️',
283
264
  },
284
265
  },
285
266
  {
@@ -290,7 +271,7 @@ const generateProblemCaseList = async (summaryResults) => {
290
271
  type: 'plain_text',
291
272
  text: 'Go to Build Details'
292
273
  },
293
- url: buildInfo.url
274
+ url: `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
294
275
  }]
295
276
  });
296
277
  } else {
@@ -375,13 +356,12 @@ const generateFailuresReasons = async (summaryResults) => {
375
356
  },
376
357
  });
377
358
  if (i === summaryResults.failures.length-1) {
378
- const buildInfo = getBuildInfo();
379
359
  failsList.push({
380
360
  type: 'section',
381
361
  text: {
382
362
  type: 'mrkdwn',
383
363
  text: '⚠️ *There are too many failures to display in full detail. '+
384
- `You can view more on ${buildInfo.platform}* ⚠️`,
364
+ 'You can view more on Github Actions* ⚠️',
385
365
  },
386
366
  },
387
367
  {
@@ -393,7 +373,7 @@ const generateFailuresReasons = async (summaryResults) => {
393
373
  type: 'plain_text',
394
374
  text: 'Go to Build Details'
395
375
  },
396
- url: buildInfo.url
376
+ url: `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
397
377
  }
398
378
  ]
399
379
  });
@@ -411,13 +391,12 @@ const generateFailuresReasons = async (summaryResults) => {
411
391
  },
412
392
  });
413
393
  if (i > 23) {
414
- const buildInfo = getBuildInfo();
415
394
  failsList.push({
416
395
  type: 'section',
417
396
  text: {
418
397
  type: 'mrkdwn',
419
398
  text: '⚠️ *There are too many failures to display here '+
420
- `You can view more on ${buildInfo.platform}* ⚠️`,
399
+ 'You can view more on Github Actions* ⚠️',
421
400
  },
422
401
  },
423
402
  {
@@ -428,7 +407,7 @@ const generateFailuresReasons = async (summaryResults) => {
428
407
  type: 'plain_text',
429
408
  text: 'Go to Build Details'
430
409
  },
431
- url: buildInfo.url
410
+ url: `https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`
432
411
  }]
433
412
  });
434
413
  break;
@@ -18,29 +18,61 @@ try {
18
18
  AdmZip = null;
19
19
  }
20
20
 
21
+ // ============================================================================
22
+ // CONSTANTS
23
+ // ============================================================================
24
+
25
+ // Directory paths
26
+ const SUMMARIES_DIR = path.join('./', 'playwright-artifacts');
27
+ const PLAYWRIGHT_REPORT_DIR = path.join('./', 'playwright-report');
28
+
29
+ // Environment variables
30
+ const IS_CI = !!process.env.CI;
31
+ const GITHUB_TOKEN = process.env.SAFETYWINGTEST_GITHUB_TOKEN;
32
+ const GITHUB_REPOSITORY = process.env.GITHUB_REPOSITORY;
33
+ const GITHUB_RUN_ID = process.env.GITHUB_RUN_ID || 'local';
34
+ const GITHUB_RUN_ATTEMPT = process.env.GITHUB_RUN_ATTEMPT || '1';
35
+
36
+ // Google Cloud Storage configuration
37
+ const GCS_BUCKET_NAME = 'sw-automation-tests';
38
+ const GCS_SERVICE_ACCOUNT_KEY = process.env.GCS_SERVICE_ACCOUNT_KEY;
39
+ const GCS_REPORTS_FOLDER_PREFIX = 'playwright-reports';
40
+ const GCS_UPLOAD_API_BASE_URL = 'https://storage.googleapis.com/upload/storage/v1';
41
+
42
+ // Artifact patterns
43
+ const ARTIFACT_NAME_PATTERNS = [
44
+ 'html-report',
45
+ 'blob-report-node',
46
+ 'test-results',
47
+ ];
48
+
49
+ // GitHub API endpoints (for artifact fetching)
50
+ const GITHUB_API_BASE_URL = 'https://api.github.com';
51
+ const GITHUB_API_ARTIFACTS_ENDPOINT = '/actions/artifacts';
52
+
53
+ // Helper functions for building GitHub API URLs and patterns
54
+ const getGitHubArtifactsApiUrl = () => `${GITHUB_API_BASE_URL}/repos/${GITHUB_REPOSITORY}${GITHUB_API_ARTIFACTS_ENDPOINT}`;
55
+ const getArtifactNamePatterns = (shardIndex) => ARTIFACT_NAME_PATTERNS.map(pattern => `${pattern}-${shardIndex}`);
56
+
57
+ // Helper functions for report paths
58
+ const getRunFolder = () => `${GCS_REPORTS_FOLDER_PREFIX}/${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}`;
59
+ const getGcsObjectPath = (relativePath) => `${getRunFolder()}/${relativePath}`;
60
+
61
+ // ============================================================================
62
+
21
63
  class ResultsParser {
22
64
  result;
23
65
  separateFlakyTests;
24
- // Handle both 0-based and 1-based shard indexing
25
- // If MATRIX_SHARD is not set, try to infer from workflow (1-based) or default to 0
26
- totalShardCount = process.env.MATRIX_COUNT ? parseInt(process.env.MATRIX_COUNT, 10) : 1;
27
- shardIndex = process.env.MATRIX_SHARD !== undefined
28
- ? parseInt(process.env.MATRIX_SHARD, 10)
29
- : (process.env.MATRIX_INDEX !== undefined ? parseInt(process.env.MATRIX_INDEX, 10) : 0);
66
+ totalShardCount = process.env.TOTAL_SHARDS
67
+ ? parseInt(process.env.TOTAL_SHARDS, 10)
68
+ : 1;
69
+ shardIndex = process.env.SHARD_INDEX !== undefined
70
+ ? parseInt(process.env.SHARD_INDEX, 10)
71
+ : 1;
30
72
 
31
- // Determine if we're using 1-based indexing (workflow-style) or 0-based
32
- // This is detected by checking if artifacts exist with 1-based naming
33
- isOneBasedIndexing = false;
34
73
  constructor(options = { separateFlakyTests: false }) {
35
74
  this.result = [];
36
75
  this.separateFlakyTests = options.separateFlakyTests;
37
- // Log shard detection in ResultsParser
38
- console.log('🔍 [ResultsParser] Initialized with shard detection:');
39
- console.log(`🔍 MATRIX_SHARD env: ${process.env.MATRIX_SHARD !== undefined ? `"${process.env.MATRIX_SHARD}"` : 'undefined'}`);
40
- console.log(`🔍 MATRIX_INDEX env: ${process.env.MATRIX_INDEX !== undefined ? `"${process.env.MATRIX_INDEX}"` : 'undefined'}`);
41
- console.log(`🔍 MATRIX_COUNT env: ${process.env.MATRIX_COUNT !== undefined ? `"${process.env.MATRIX_COUNT}"` : 'undefined'}`);
42
- console.log(`🔍 Calculated shardIndex: ${this.shardIndex}`);
43
- console.log(`🔍 Calculated totalShardCount: ${this.totalShardCount}`);
44
76
  }
45
77
  async getParsedResults() {
46
78
  const summary = {
@@ -60,17 +92,15 @@ class ResultsParser {
60
92
  }*/
61
93
 
62
94
  // Define the directory to store node summaries
63
- const summariesDir = path.join('./', 'playwright-report');
64
-
65
- if (!fs.existsSync(summariesDir)) {
66
- fs.mkdirSync(summariesDir, { recursive: true });
95
+ if (!fs.existsSync(PLAYWRIGHT_REPORT_DIR)) {
96
+ fs.mkdirSync(PLAYWRIGHT_REPORT_DIR, { recursive: true });
67
97
  }
68
98
 
69
99
  // Determine the current node index
70
100
  const currentNodeIndex = this.shardIndex;
71
101
 
72
102
  // Define the file for the current node's summary
73
- const nodeSummaryFile = path.join(summariesDir, `node_summary_${currentNodeIndex}.json`);
103
+ const nodeSummaryFile = path.join(PLAYWRIGHT_REPORT_DIR, `node_summary_${currentNodeIndex}.json`);
74
104
 
75
105
  const totalTestCasesForNode = this.result.reduce((acc, suite) => acc + suite.testSuite.tests.length, 0);
76
106
 
@@ -97,30 +127,22 @@ class ResultsParser {
97
127
  // Create the file
98
128
  fs.writeFileSync(nodeSummaryFile, JSON.stringify(nodeSummary, null, 2));
99
129
 
100
- console.log(`🔍 [ResultsParser] Current shard: ${this.shardIndex}, Total shards: ${this.totalShardCount}`);
101
- console.log(`🔍 [ResultsParser] Created node summary file: ${nodeSummaryFile}`);
102
-
103
- if (this.shardIndex === 0 && this.totalShardCount > 1) {
104
- console.log(`🔍 [ResultsParser] Shard 0 detected with ${this.totalShardCount} total shards - will fetch artifacts from other shards`);
105
- if (process.env.CI) {
106
- console.log('🔍 [ResultsParser] CI environment detected - fetching all artifacts from GitHub Actions...');
130
+ if (this.shardIndex === 1 && this.totalShardCount > 1) {
131
+ if (IS_CI) {
132
+ console.log('Fetching all artifacts...');
107
133
  await this.fetchAllArtifacts();
108
134
  } else {
109
- console.log('🔍 [ResultsParser] Local environment - waiting for artifacts to appear...');
110
135
  while (!this.allNodeSummaryFilesExist() || !this.allBlobZipsExist()) {
111
- console.log('🔍 [ResultsParser] Waiting for all artifacts to exist...');
136
+ console.log('Waiting for all both to exist...');
112
137
  await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for 1 second
113
138
  }
114
139
  }
115
- } else {
116
- console.log(`🔍 [ResultsParser] Not shard 0 (current: ${this.shardIndex}) or single shard (total: ${this.totalShardCount}) - skipping artifact fetch`);
117
140
  }
118
141
 
119
- if (this.shardIndex === 0 && this.allNodeSummaryFilesExist() && this.allBlobZipsExist()) {
120
- console.log(`🔍 [ResultsParser] Shard 0: All artifacts available, merging results from all shards...`);
142
+ if (this.shardIndex === 1 && this.allNodeSummaryFilesExist() && this.allBlobZipsExist()) {
121
143
  // Merge all node summaries into the final summary
122
- for (let i = 0; i < this.totalShardCount; i++) {
123
- const nodeSummaryFile = path.join(summariesDir, `node_summary_${i}.json`);
144
+ for (let i = 1; i <= this.totalShardCount; i++) {
145
+ const nodeSummaryFile = path.join(SUMMARIES_DIR, `node_summary_${i}.json`);
124
146
  const fileToRead = nodeSummaryFile;
125
147
 
126
148
  if (fs.existsSync(fileToRead)) {
@@ -155,7 +177,8 @@ class ResultsParser {
155
177
  }
156
178
  }
157
179
 
158
- this.mergeReports();
180
+ await this.mergeReports();
181
+ await this.pushReportsToGCS();
159
182
  }
160
183
  return summary;
161
184
  }
@@ -332,10 +355,9 @@ class ResultsParser {
332
355
  }*/
333
356
  allNodeSummaryFilesExist() {
334
357
  console.log('Checking if all node summary files exist...');
335
- const summariesDir = path.join('./', 'playwright-report');
336
358
 
337
- for (let i = 0; i < this.totalShardCount; i++) {
338
- const nodeSummaryFile = path.join(summariesDir, `node_summary_${i}.json`);
359
+ for (let i = 1; i <= this.totalShardCount; i++) {
360
+ const nodeSummaryFile = path.join(SUMMARIES_DIR, `node_summary_${i}.json`);
339
361
  if (!fs.existsSync(nodeSummaryFile)) {
340
362
  return false;
341
363
  }
@@ -345,10 +367,9 @@ class ResultsParser {
345
367
 
346
368
  allBlobZipsExist() {
347
369
  console.log('Checking if all blob zips exist...');
348
- const summariesDir = path.join('./', 'playwright-report');
349
370
 
350
- for (let i = 0; i < this.totalShardCount; i++) {
351
- const blobZipFile = path.join(summariesDir, `blob-report-node-${i}.zip`);
371
+ for (let i = 1; i <= this.totalShardCount; i++) {
372
+ const blobZipFile = path.join(SUMMARIES_DIR, `blob-report-node-${i}.zip`);
352
373
  if (!fs.existsSync(blobZipFile)) {
353
374
  return false;
354
375
  }
@@ -357,109 +378,49 @@ class ResultsParser {
357
378
  }
358
379
 
359
380
  async fetchAllArtifacts() {
360
- const summariesDir = path.join('./', 'playwright-report');
361
- if (!fs.existsSync(summariesDir)) {
362
- fs.mkdirSync(summariesDir, { recursive: true });
363
- }
364
-
365
- console.log(`Shard 0: Waiting for artifacts from ${this.totalShardCount - 1} other shard(s)...`);
366
-
367
- // Fetch artifacts for shards 1 to totalShardCount-1 (since we're shard 0)
368
- // We need to fetch from all other shards before proceeding
369
- for (let i = 1; i < this.totalShardCount; i++) {
370
- console.log(`Shard 0: Fetching artifacts from shard ${i}...`);
371
- await this.fetchArtifact(i, `node_summary_${i}.json`, summariesDir);
372
- await this.fetchArtifact(i, `blob-report-node-${i}.zip`, summariesDir);
381
+ // Ensure summaries directory exists
382
+ if (!fs.existsSync(SUMMARIES_DIR)) {
383
+ fs.mkdirSync(SUMMARIES_DIR, { recursive: true });
373
384
  }
374
385
 
375
- // After fetching, verify all files exist and wait if needed
376
- let retryCount = 0;
377
- const maxRetries = 60; // Wait up to 10 minutes (60 * 10 seconds)
378
-
379
- while ((!this.allNodeSummaryFilesExist() || !this.allBlobZipsExist()) && retryCount < maxRetries) {
380
- console.log(`Shard 0: Waiting for all artifacts to be available (attempt ${retryCount + 1}/${maxRetries})...`);
381
-
382
- // Re-fetch any missing artifacts
383
- for (let i = 1; i < this.totalShardCount; i++) {
384
- const nodeSummaryFile = path.join(summariesDir, `node_summary_${i}.json`);
385
- const blobZipFile = path.join(summariesDir, `blob-report-node-${i}.zip`);
386
-
387
- if (!fs.existsSync(nodeSummaryFile)) {
388
- console.log(`Shard 0: Re-fetching missing node_summary_${i}.json...`);
389
- await this.fetchArtifact(i, `node_summary_${i}.json`, summariesDir);
390
- }
391
-
392
- if (!fs.existsSync(blobZipFile)) {
393
- console.log(`Shard 0: Re-fetching missing blob-report-node-${i}.zip...`);
394
- await this.fetchArtifact(i, `blob-report-node-${i}.zip`, summariesDir);
395
- }
396
- }
397
-
398
- retryCount++;
399
- if (!this.allNodeSummaryFilesExist() || !this.allBlobZipsExist()) {
400
- await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds before retry
401
- }
386
+ // Copy entire contents of playwright-report to SUMMARIES_DIR
387
+ // This copies both blob-report-node-1.zip and node_summary_1.json to SUMMARIES_DIR
388
+ const playwrightReportDir = path.join('./', 'playwright-report');
389
+ if (fs.existsSync(playwrightReportDir)) {
390
+ this.copyDirectoryRecursive(playwrightReportDir, SUMMARIES_DIR);
391
+ console.log(`Copied entire contents of playwright-report to ${SUMMARIES_DIR}`);
392
+ } else {
393
+ console.warn('Warning: playwright-report directory not found');
402
394
  }
403
395
 
404
- if (this.allNodeSummaryFilesExist() && this.allBlobZipsExist()) {
405
- console.log(`Shard 0: Successfully fetched all artifacts from ${this.totalShardCount - 1} other shard(s).`);
406
- } else {
407
- console.warn(`Shard 0: Warning - Some artifacts may still be missing after ${maxRetries} attempts.`);
396
+ while (!this.allNodeSummaryFilesExist() || !this.allBlobZipsExist()) {
397
+ console.log('Checking if all node summary files exist...');
398
+ console.log('Waiting for all blob zips to exist...');
399
+ if (!fs.existsSync(SUMMARIES_DIR)) {
400
+ fs.mkdirSync(SUMMARIES_DIR, { recursive: true });
401
+ }
402
+ for (let i = 2; i <= this.totalShardCount; i++) {
403
+ await this.fetchArtifact(i, `node_summary_${i}.json`, SUMMARIES_DIR);
404
+ await this.fetchArtifact(i, `blob-report-node-${i}.zip`, SUMMARIES_DIR);
405
+ }
408
406
  }
409
407
  }
410
408
 
411
- async fetchArtifact(i, file, summariesDir) {
412
- const filePath = path.join(summariesDir, file);
409
+ async fetchArtifact(i, file, targetDir) {
410
+ const filePath = path.join(targetDir, file);
413
411
  await this.fetchArtifactFromGitHubActions(i, file, filePath);
414
412
  }
415
413
 
416
- // Helper to extract shard index from artifact name
417
- extractShardIndexFromArtifactName(artifactName, i) {
418
- // Try to extract number from artifact name (e.g., html-report-1 -> 1)
419
- const match = artifactName.match(/-(\d+)$/);
420
- if (match) {
421
- const artifactShard = parseInt(match[1], 10);
422
- // If artifact uses 1-based indexing, convert to 0-based for internal use
423
- // html-report-1 (workflow) -> shard 0 (internal)
424
- // html-report-2 (workflow) -> shard 1 (internal)
425
- return artifactShard - 1;
426
- }
427
- // Fallback to the provided index
428
- return i;
429
- }
430
-
431
414
  async fetchArtifactFromGitHubActions(i, file, filePath) {
432
- const githubToken = process.env.SAFETYWINGTEST_GITHUB_TOKEN;
433
- const repo = process.env.GITHUB_REPOSITORY;
434
- const runId = process.env.GITHUB_RUN_ID;
435
-
436
- if (!githubToken || !repo) {
437
- console.error('GitHub Actions: Missing SAFETYWINGTEST_GITHUB_TOKEN or GITHUB_REPOSITORY');
438
- return;
439
- }
440
-
441
- // GitHub Actions artifact API endpoint
442
- const githubApiUrl = `https://api.github.com/repos/${repo}/actions/artifacts`;
443
-
444
- // Try multiple artifact naming patterns:
445
- // 1. html-report-{i} (1-based workflow style)
446
- // 2. blob-report-node-{i} (direct blob reports)
447
- // 3. test-results-{i} (alternative naming)
448
- // Note: i is 0-based internally, so for 1-based workflows we need i+1
449
- const artifactNamePatterns = [
450
- `html-report-${i + 1}`, // 1-based workflow style (most common)
451
- `blob-report-node-${i}`, // 0-based direct blob
452
- `blob-report-node-${i + 1}`, // 1-based direct blob
453
- `html-report-${i}`, // 0-based html-report
454
- `test-results-${i + 1}`, // Alternative naming
455
- ];
415
+ const githubApiUrl = getGitHubArtifactsApiUrl();
416
+ const artifactNamePatterns = getArtifactNamePatterns(i);
456
417
 
457
418
  while (true) {
458
419
  try {
459
420
  // List all artifacts to find the one we need
460
421
  const listResponse = await axios.get(githubApiUrl, {
461
422
  headers: {
462
- 'Authorization': `token ${githubToken}`,
423
+ 'Authorization': `token ${GITHUB_TOKEN}`,
463
424
  'Accept': 'application/vnd.github.v3+json'
464
425
  },
465
426
  params: {
@@ -472,22 +433,17 @@ class ResultsParser {
472
433
  for (const pattern of artifactNamePatterns) {
473
434
  artifact = listResponse.data.artifacts.find(
474
435
  (a) => a.name === pattern ||
475
- a.name.includes(`shard-${i}`) ||
476
- a.name.includes(`shard-${i + 1}`) ||
477
- a.name.includes(`node-${i}`) ||
478
- a.name.includes(`node-${i + 1}`)
436
+ a.name.includes(`shard-${i}`) ||
437
+ a.name.includes(`node-${i}`)
479
438
  );
480
- if (artifact) {
481
- console.log(`Found artifact: ${artifact.name} (matched pattern: ${pattern})`);
482
- break;
483
- }
439
+ if (artifact) break;
484
440
  }
485
441
 
486
442
  if (artifact && artifact.archive_download_url) {
487
443
  // Download the artifact ZIP
488
444
  const downloadResponse = await axios.get(artifact.archive_download_url, {
489
445
  headers: {
490
- 'Authorization': `token ${githubToken}`,
446
+ 'Authorization': `token ${GITHUB_TOKEN}`,
491
447
  'Accept': 'application/vnd.github.v3+json'
492
448
  },
493
449
  responseType: 'arraybuffer'
@@ -500,113 +456,15 @@ class ResultsParser {
500
456
 
501
457
  // Extract ZIP and find the specific file
502
458
  const zip = new AdmZip(downloadResponse.data);
503
-
504
- // Try direct file match first
505
- let zipEntry = zip.getEntry(file);
506
-
507
- // If not found, try common path variations
508
- if (!zipEntry) {
509
- const possiblePaths = [
510
- file,
511
- `playwright-report/${file}`,
512
- `playwright-report/${file}`,
513
- `html-report-${i + 1}/${file}`,
514
- `html-report-${i}/${file}`,
515
- ];
516
- for (const possiblePath of possiblePaths) {
517
- zipEntry = zip.getEntry(possiblePath);
518
- if (zipEntry) break;
519
- }
520
- }
459
+ const zipEntry = zip.getEntry(file) || zip.getEntry(`playwright-artifacts/${file}`);
521
460
 
522
461
  if (zipEntry) {
523
462
  fs.writeFileSync(filePath, zipEntry.getData());
524
- console.log(`Successfully fetched file ${file} from GitHub Actions shard ${i} (artifact: ${artifact.name})`);
463
+ console.log(`Successfully fetched file ${file} from shard ${i}`);
525
464
  break;
526
465
  } else {
527
- // If file not found in ZIP root, extract and search recursively
528
- const extractPath = path.join(path.dirname(filePath), `temp-artifact-${i}`);
529
- zip.extractAllTo(extractPath, true);
530
-
531
- // Extract shard index from artifact name to handle files named with index 0
532
- const artifactShardIndex = this.extractShardIndexFromArtifactName(artifact.name, i);
533
-
534
- // Look for the file recursively, also try with shard index from artifact name
535
- const searchInDir = (dir, targetFile, alternateTargetFile = null) => {
536
- const entries = fs.readdirSync(dir, { withFileTypes: true });
537
- for (const entry of entries) {
538
- const fullPath = path.join(dir, entry.name);
539
- if (entry.isDirectory()) {
540
- const found = searchInDir(fullPath, targetFile, alternateTargetFile);
541
- if (found) return found;
542
- } else {
543
- // Check exact match
544
- if (entry.name === targetFile) {
545
- return fullPath;
546
- }
547
- // Check alternate filename (e.g., node_summary_0.json when we need node_summary_1.json)
548
- if (alternateTargetFile && entry.name === alternateTargetFile) {
549
- return fullPath;
550
- }
551
- // Check if ends with target file
552
- if (entry.name.endsWith(targetFile)) {
553
- return fullPath;
554
- }
555
- }
556
- }
557
- return null;
558
- };
559
-
560
- // Try to find the file, also try with shard index from artifact
561
- let foundPath = searchInDir(extractPath, file);
562
-
563
- // If file uses index 0 pattern (like node_summary_0.json or blob-report-node-0.zip)
564
- // but we need it for a different shard, try to find any matching pattern
565
- if (!foundPath && (file.includes('_0') || file.includes('-node-0'))) {
566
- // Recursively search for any node_summary or blob-report file
567
- const findAllFiles = (dir, pattern) => {
568
- const results = [];
569
- try {
570
- const entries = fs.readdirSync(dir, { withFileTypes: true });
571
- for (const entry of entries) {
572
- const fullPath = path.join(dir, entry.name);
573
- if (entry.isDirectory()) {
574
- results.push(...findAllFiles(fullPath, pattern));
575
- } else if (entry.name.includes(pattern)) {
576
- results.push(fullPath);
577
- }
578
- }
579
- } catch (e) {
580
- // Ignore errors
581
- }
582
- return results;
583
- };
584
-
585
- // Find any node_summary or blob-report files
586
- const matchingFiles = findAllFiles(extractPath, file.includes('node_summary') ? 'node_summary' : 'blob-report');
587
-
588
- if (matchingFiles.length > 0) {
589
- foundPath = matchingFiles[0];
590
- // Copy to the expected path with correct shard index
591
- fs.copyFileSync(foundPath, filePath);
592
- console.log(`Found and copied file ${path.basename(foundPath)} -> ${file} from artifact ${artifact.name}`);
593
- // Clean up temp directory
594
- fs.rmSync(extractPath, { recursive: true, force: true });
595
- break;
596
- }
597
- }
598
-
599
- if (foundPath) {
600
- fs.copyFileSync(foundPath, filePath);
601
- console.log(`Successfully fetched file ${file} from GitHub Actions shard ${i} (artifact: ${artifact.name})`);
602
- // Clean up temp directory
603
- fs.rmSync(extractPath, { recursive: true, force: true });
604
- break;
605
- } else {
606
- console.warn(`File ${file} not found in artifact ${artifact.name}. Retrying in 10 seconds...`);
607
- fs.rmSync(extractPath, { recursive: true, force: true });
608
- await new Promise(resolve => setTimeout(resolve, 10000));
609
- }
466
+ console.warn(`File ${file} not found in artifact ${artifact.name}. Retrying in 10 seconds...`);
467
+ await new Promise(resolve => setTimeout(resolve, 10000));
610
468
  }
611
469
  } else {
612
470
  console.warn(`Artifact not found (tried: ${artifactNamePatterns.join(', ')}). Retrying in 10 seconds...`);
@@ -614,22 +472,39 @@ class ResultsParser {
614
472
  }
615
473
  } catch (error) {
616
474
  if (error.response && error.response.status === 404) {
617
- console.warn(`Artifact not found. Retrying in 10 seconds...`);
475
+ console.warn(`File ${file} not found. Retrying in 10 seconds...`);
618
476
  await new Promise(resolve => setTimeout(resolve, 10000));
619
477
  } else {
620
- console.error(`Failed to fetch artifact from GitHub Actions shard ${i}!`, error.message);
621
- await new Promise(resolve => setTimeout(resolve, 10000));
478
+ console.error(`Failed to fetch file ${file} from shard ${i}!`, error);
479
+ break;
622
480
  }
623
481
  }
624
482
  }
625
483
  }
626
- mergeReports() {
484
+ async mergeReports() {
627
485
  let breakMergeWaiter = false;
628
486
  try {
629
- // Execute the command to merge reports
630
- exec(`npx playwright merge-reports --reporter html -c ./playwright-report playwright-report`);
631
- // Wait until index.html exists
632
- while (!fs.existsSync(path.join('./playwright-report', 'index.html'))) {
487
+
488
+ // Copy all blob files to playwright-report before merging
489
+ for (let i = 1; i <= this.totalShardCount; i++) {
490
+ const blobZipFile = path.join(SUMMARIES_DIR, `blob-report-node-${i}.zip`);
491
+ if (fs.existsSync(blobZipFile)) {
492
+ try {
493
+ const destFile = path.join(PLAYWRIGHT_REPORT_DIR, `blob-report-node-${i}.zip`);
494
+ fs.copyFileSync(blobZipFile, destFile);
495
+ console.log(`Copied blob-report-node-${i}.zip to playwright-report for merging`);
496
+ } catch (error) {
497
+ console.warn(`Failed to copy blob-report-node-${i}.zip for merging:`, error.message);
498
+ }
499
+ }
500
+ }
501
+
502
+ // Execute the command to merge reports FROM playwright-report
503
+ // Playwright will output to playwright-report directory
504
+ exec(`npx playwright merge-reports --reporter html ${PLAYWRIGHT_REPORT_DIR}`);
505
+
506
+ // Wait until index.html exists in playwright-report
507
+ while (!fs.existsSync(path.join(PLAYWRIGHT_REPORT_DIR, 'index.html'))) {
633
508
  console.log('Waiting 2 seconds for merged html report to be generated...');
634
509
  const currentTime = new Date().getTime();
635
510
  breakMergeWaiter = false;
@@ -640,10 +515,209 @@ class ResultsParser {
640
515
  }
641
516
  }
642
517
  console.log('Reports merged successfully.');
518
+
519
+ // After merge, extract all blobs from SUMMARIES_DIR to playwright-report
520
+ this.unzipBlobsAndCopyResources(SUMMARIES_DIR, PLAYWRIGHT_REPORT_DIR);
643
521
  } catch (error) {
644
522
  // Log a warning instead of throwing an error
645
523
  console.warn('Warning: Failed to merge reports. This may not affect the overall process.', error.message);
646
524
  }
647
525
  }
526
+
527
+ copyDirectoryRecursive(srcDir, destDir) {
528
+ if (!fs.existsSync(destDir)) {
529
+ fs.mkdirSync(destDir, { recursive: true });
530
+ }
531
+
532
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
533
+
534
+ for (const entry of entries) {
535
+ const srcPath = path.join(srcDir, entry.name);
536
+ const destPath = path.join(destDir, entry.name);
537
+
538
+ if (entry.isDirectory()) {
539
+ this.copyDirectoryRecursive(srcPath, destPath);
540
+ } else {
541
+ fs.copyFileSync(srcPath, destPath);
542
+ }
543
+ }
544
+ }
545
+
546
+ unzipBlobsAndCopyResources(ARTIFACTS_DIR, FINAL_OUTPUT_DIR) {
547
+ if (!AdmZip) {
548
+ console.error('adm-zip is required for blob extraction. Please install it: npm install adm-zip');
549
+ return;
550
+ }
551
+
552
+ // Unzip all blob files into separate folders
553
+ for (let i = 1; i <= this.totalShardCount; i++) {
554
+ const blobZipFile = path.join(ARTIFACTS_DIR, `blob-report-node-${i}.zip`);
555
+ if (fs.existsSync(blobZipFile)) {
556
+ const extractDir = path.join(ARTIFACTS_DIR, `${GITHUB_RUN_ID}-blob-contents-node-${i}`);
557
+ if (!fs.existsSync(extractDir)) {
558
+ fs.mkdirSync(extractDir, { recursive: true });
559
+ }
560
+
561
+ try {
562
+ const zip = new AdmZip(blobZipFile);
563
+ zip.extractAllTo(extractDir, true);
564
+ console.log(`Extracted blob-report-node-${i}.zip to ${extractDir}`);
565
+
566
+ // Check if this folder contains a "resources" folder with zip files
567
+ const resourcesDir = path.join(extractDir, 'resources');
568
+ if (fs.existsSync(resourcesDir) && fs.statSync(resourcesDir).isDirectory()) {
569
+ const files = fs.readdirSync(resourcesDir);
570
+ const zipFiles = files.filter(file => file.endsWith('.zip'));
571
+
572
+ if (zipFiles.length > 0) {
573
+ // Copy the resources folder to finalOutput
574
+ const finalResourcesDir = path.join(FINAL_OUTPUT_DIR, 'resources');
575
+ if (!fs.existsSync(finalResourcesDir)) {
576
+ fs.mkdirSync(finalResourcesDir, { recursive: true });
577
+ }
578
+
579
+ // Copy each zip file to finalOutput/resources
580
+ for (const zipFile of zipFiles) {
581
+ const sourceFile = path.join(resourcesDir, zipFile);
582
+ const destFile = path.join(finalResourcesDir, zipFile);
583
+ fs.copyFileSync(sourceFile, destFile);
584
+ console.log(`Copied ${zipFile} to finalOutput/resources`);
585
+ }
586
+ }
587
+ }
588
+ } catch (error) {
589
+ console.warn(`Failed to extract or process blob-report-node-${i}.zip:`, error.message);
590
+ }
591
+ }
592
+ }
593
+ }
594
+
595
+ async getGcsAccessToken() {
596
+ if (!GCS_SERVICE_ACCOUNT_KEY) {
597
+ throw new Error('GCS_SERVICE_ACCOUNT_KEY environment variable is required');
598
+ }
599
+
600
+ let serviceAccount;
601
+ try {
602
+ serviceAccount = JSON.parse(GCS_SERVICE_ACCOUNT_KEY);
603
+ } catch (e) {
604
+ // Try reading from file path
605
+ if (fs.existsSync(GCS_SERVICE_ACCOUNT_KEY)) {
606
+ serviceAccount = JSON.parse(fs.readFileSync(GCS_SERVICE_ACCOUNT_KEY, 'utf-8'));
607
+ } else {
608
+ throw new Error('Invalid GCS_SERVICE_ACCOUNT_KEY: must be JSON string or file path');
609
+ }
610
+ }
611
+
612
+ // Request OAuth2 token from Google
613
+ const jwt = require('jsonwebtoken');
614
+ const now = Math.floor(Date.now() / 1000);
615
+ const token = jwt.sign(
616
+ {
617
+ iss: serviceAccount.client_email,
618
+ scope: 'https://www.googleapis.com/auth/devstorage.full_control',
619
+ aud: 'https://oauth2.googleapis.com/token',
620
+ exp: now + 3600,
621
+ iat: now
622
+ },
623
+ serviceAccount.private_key,
624
+ { algorithm: 'RS256' }
625
+ );
626
+
627
+ const tokenResponse = await axios.post('https://oauth2.googleapis.com/token', {
628
+ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
629
+ assertion: token
630
+ });
631
+
632
+ return tokenResponse.data.access_token;
633
+ }
634
+
635
+ async pushReportsToGCS() {
636
+ // Only push in CI environment with required configuration
637
+ if (!IS_CI || !GCS_SERVICE_ACCOUNT_KEY) {
638
+ console.log('Skipping GCS push: not in CI or missing GCS_SERVICE_ACCOUNT_KEY');
639
+ return;
640
+ }
641
+
642
+ try {
643
+ console.log(`Uploading playwright-report files to GCS bucket: ${GCS_BUCKET_NAME} in folder: ${getRunFolder()}`);
644
+
645
+ // Get access token
646
+ const accessToken = await this.getGcsAccessToken();
647
+
648
+ // Collect all files from playwright-report directory
649
+ const files = this.getAllFilesRecursive(PLAYWRIGHT_REPORT_DIR);
650
+
651
+ // Normalize the base directory path for path replacement
652
+ const baseDirPath = path.resolve(PLAYWRIGHT_REPORT_DIR);
653
+
654
+ // Upload all files to GCS
655
+ for (const file of files) {
656
+ // Get relative path from playwright-report directory
657
+ const relativePath = path.relative(baseDirPath, file).replace(/\\/g, '/');
658
+ const objectPath = getGcsObjectPath(relativePath);
659
+ const content = fs.readFileSync(file);
660
+
661
+ // Upload file to GCS
662
+ const uploadUrl = `${GCS_UPLOAD_API_BASE_URL}/b/${GCS_BUCKET_NAME}/o?uploadType=media&name=${encodeURIComponent(objectPath)}`;
663
+
664
+ await axios.post(uploadUrl, content, {
665
+ headers: {
666
+ 'Authorization': `Bearer ${accessToken}`,
667
+ 'Content-Type': this.getContentType(file)
668
+ }
669
+ });
670
+
671
+ console.log(`Uploaded ${objectPath} to GCS`);
672
+ }
673
+
674
+ console.log(`Successfully uploaded ${files.length} files to GCS bucket: ${GCS_BUCKET_NAME} in folder: ${getRunFolder()}`);
675
+ } catch (error) {
676
+ console.warn('Warning: Failed to push reports to GCS. This may not affect the overall process.', error.message);
677
+ if (error.response) {
678
+ console.warn('GCS API error:', error.response.data);
679
+ }
680
+ }
681
+ }
682
+
683
+ getContentType(filePath) {
684
+ const ext = path.extname(filePath).toLowerCase();
685
+ const contentTypes = {
686
+ '.html': 'text/html',
687
+ '.css': 'text/css',
688
+ '.js': 'application/javascript',
689
+ '.json': 'application/json',
690
+ '.png': 'image/png',
691
+ '.jpg': 'image/jpeg',
692
+ '.jpeg': 'image/jpeg',
693
+ '.gif': 'image/gif',
694
+ '.svg': 'image/svg+xml',
695
+ '.zip': 'application/zip',
696
+ '.txt': 'text/plain',
697
+ '.xml': 'application/xml'
698
+ };
699
+ return contentTypes[ext] || 'application/octet-stream';
700
+ }
701
+
702
+ getAllFilesRecursive(dir, fileList = []) {
703
+ if (!fs.existsSync(dir)) {
704
+ return fileList;
705
+ }
706
+
707
+ const files = fs.readdirSync(dir);
708
+
709
+ for (const file of files) {
710
+ const filePath = path.join(dir, file);
711
+ const stat = fs.statSync(filePath);
712
+
713
+ if (stat.isDirectory()) {
714
+ this.getAllFilesRecursive(filePath, fileList);
715
+ } else {
716
+ fileList.push(filePath);
717
+ }
718
+ }
719
+
720
+ return fileList;
721
+ }
648
722
  }
649
723
  exports.default = ResultsParser;
@@ -6,13 +6,7 @@ const webhook_1 = require("@slack/webhook");
6
6
  const ResultsParser_1 = require("./ResultsParser");
7
7
  const SlackClient_1 = require("./SlackClient");
8
8
  const SlackWebhookClient_1 = require("./SlackWebhookClient");
9
- let axios;
10
- try {
11
- axios = require('axios');
12
- } catch (e) {
13
- // axios optional for GitHub Actions API detection
14
- axios = null;
15
- }
9
+
16
10
  class SlackReporter {
17
11
  customLayout;
18
12
  customLayoutAsync;
@@ -71,303 +65,18 @@ class SlackReporter {
71
65
  onTestEnd(test, result) {
72
66
  this.resultsParser.addTestResult(test.parent.title, test, this.browsers);
73
67
  }
74
- /**
75
- * Detect GitHub Actions shard information using GitHub API
76
- * Returns { shardIndex: number, totalShards: number } or null if detection fails
77
- */
78
- async detectGitHubActionsShardInfo() {
79
- // Check if we're in GitHub Actions
80
- if (!process.env.GITHUB_ACTIONS || process.env.GITHUB_ACTIONS !== 'true') {
81
- return null;
82
- }
83
- // Try GITHUB_TOKEN first (automatic), then fallback to custom token
84
- const githubToken = process.env.GITHUB_TOKEN || process.env.SAFETYWINGTEST_GITHUB_TOKEN;
85
- const repo = process.env.GITHUB_REPOSITORY;
86
- const runId = process.env.GITHUB_RUN_ID;
87
- const currentJobId = process.env.GITHUB_JOB;
88
- if (!githubToken || !repo || !runId) {
89
- this.log(`🔍 [GitHub Actions Detection] Missing required env vars: GITHUB_TOKEN=${!!process.env.GITHUB_TOKEN}, SAFETYWINGTEST_GITHUB_TOKEN=${!!process.env.SAFETYWINGTEST_GITHUB_TOKEN}, GITHUB_REPOSITORY=${!!repo}, GITHUB_RUN_ID=${!!runId}`);
90
- return null;
91
- }
92
- this.log(`🔍 [GitHub Actions Detection] Using token: ${process.env.GITHUB_TOKEN ? 'GITHUB_TOKEN' : 'SAFETYWINGTEST_GITHUB_TOKEN'}`);
93
- this.log(`🔍 [GitHub Actions Detection] Repository: ${repo}, Run ID: ${runId}`);
94
- if (!axios) {
95
- this.log(`🔍 [GitHub Actions Detection] axios not available, skipping API detection`);
96
- return null;
97
- }
98
- try {
99
- // Get workflow run details
100
- const apiUrl = `https://api.github.com/repos/${repo}/actions/runs/${runId}/jobs`;
101
- this.log(`🔍 [GitHub Actions Detection] API URL: ${apiUrl}`);
102
- const response = await axios.get(apiUrl, {
103
- headers: {
104
- 'Accept': 'application/vnd.github+json',
105
- 'Authorization': `Bearer ${githubToken}`,
106
- 'X-GitHub-Api-Version': '2022-11-28'
107
- },
108
- params: {
109
- per_page: 100
110
- }
111
- });
112
- const jobs = response.data.jobs || [];
113
- if (jobs.length === 0) {
114
- this.log(`🔍 [GitHub Actions Detection] No jobs found in workflow run`);
115
- return null;
116
- }
117
-
118
- this.log(`🔍 [GitHub Actions Detection] Found ${jobs.length} total jobs in workflow run`);
119
-
120
- // Helper function to normalize job names by removing shard indicators
121
- // e.g., "Run tests (shard 3/4)" -> "Run tests"
122
- const normalizeJobName = (name) => {
123
- if (!name) return '';
124
- // Remove common matrix patterns: (shard X/Y), (X, Y), [X], etc.
125
- return name
126
- .replace(/\s*\(shard\s+\d+\/\d+\)/gi, '')
127
- .replace(/\s*\(\d+\/\d+\)/g, '')
128
- .replace(/\s*\(\d+,\s*\d+\)/g, '')
129
- .replace(/\s*\[\d+\]/g, '')
130
- .trim();
131
- };
132
-
133
- // Group jobs by normalized name to find matrix jobs
134
- const jobGroups = {};
135
- for (const job of jobs) {
136
- const jobName = job.name || '';
137
- const normalizedName = normalizeJobName(jobName);
138
- if (!jobGroups[normalizedName]) {
139
- jobGroups[normalizedName] = [];
140
- }
141
- jobGroups[normalizedName].push(job);
142
- }
143
-
144
- this.log(`🔍 [GitHub Actions Detection] Grouped into ${Object.keys(jobGroups).length} job group(s)`);
145
- for (const [groupName, groupJobs] of Object.entries(jobGroups)) {
146
- this.log(`🔍 Group "${groupName}": ${groupJobs.length} job(s)`);
147
- }
148
-
149
- // Find the job group that contains the current job
150
- let currentJob = null;
151
- let jobGroup = null;
152
-
153
- // First, try to find by job name match
154
- for (const job of jobs) {
155
- if (job.name && (job.name.includes(currentJobId) || currentJobId.includes(job.name))) {
156
- currentJob = job;
157
- const normalizedName = normalizeJobName(job.name);
158
- jobGroup = jobGroups[normalizedName];
159
- this.log(`🔍 [GitHub Actions Detection] Matched current job by name: "${job.name}"`);
160
- break;
161
- }
162
- }
163
-
164
- // If still no match, use all jobs as fallback
165
- if (!jobGroup || jobGroup.length === 0) {
166
- this.log(`🔍 [GitHub Actions Detection] Could not match job, using all jobs as group`);
167
- jobGroup = jobs;
168
- currentJob = jobs[0];
169
- }
170
- // Sort jobs by name or ID to get consistent ordering
171
- jobGroup.sort((a, b) => {
172
- if (a.name && b.name) {
173
- return a.name.localeCompare(b.name);
174
- }
175
- return a.id - b.id;
176
- });
177
-
178
- // Find index of current job in the sorted group
179
- const currentJobIndex = jobGroup.findIndex(j => j.id === currentJob?.id || j.name === currentJob?.name);
180
-
181
- // Try to extract shard number from job name (e.g., "shard 3/4" -> index 2)
182
- let shardIndex = currentJobIndex >= 0 ? currentJobIndex : 0;
183
- if (currentJob && currentJob.name) {
184
- const shardMatch = currentJob.name.match(/shard\s+(\d+)\/(\d+)/i);
185
- if (shardMatch) {
186
- const shardNum = parseInt(shardMatch[1], 10);
187
- const totalFromName = parseInt(shardMatch[2], 10);
188
- // Convert to 0-based index (shard 3/4 -> index 2)
189
- shardIndex = shardNum - 1;
190
- this.log(`🔍 [GitHub Actions Detection] Extracted shard info from name: ${shardNum}/${totalFromName} -> index ${shardIndex}`);
191
- }
192
- }
193
-
194
- const totalShards = jobGroup.length;
195
- this.log(`🔍 [GitHub Actions Detection] Detected via API:`);
196
- this.log(`🔍 Total jobs in group: ${totalShards}`);
197
- this.log(`🔍 Current job index: ${shardIndex}`);
198
- this.log(`🔍 Current job ID: ${currentJobId}`);
199
- this.log(`🔍 Current job name: ${currentJob?.name || 'unknown'}`);
200
- return {
201
- shardIndex,
202
- totalShards
203
- };
204
- } catch (error) {
205
- const apiUrl = `https://api.github.com/repos/${repo}/actions/runs/${runId}/jobs`;
206
- this.log(`🔍 [GitHub Actions Detection] Failed to detect via API: ${error.message}`);
207
- if (error.response) {
208
- const status = error.response.status;
209
- const data = error.response.data;
210
- this.log(`🔍 Status: ${status}, URL: ${apiUrl}`);
211
-
212
- if (status === 404) {
213
- this.log(`🔍 404 Error Details:`);
214
- this.log(`🔍 - Repository: ${repo}`);
215
- this.log(`🔍 - Run ID: ${runId}`);
216
- this.log(`🔍 - Possible causes:`);
217
- this.log(`🔍 1. Token lacks 'actions: read' permission`);
218
- this.log(`🔍 2. Workflow run doesn't exist or isn't accessible`);
219
- this.log(`🔍 3. Jobs not available yet (try accessing after workflow starts)`);
220
- this.log(`🔍 - Recommendation: Set MATRIX_SHARD and MATRIX_COUNT env vars as fallback`);
221
- } else if (status === 401 || status === 403) {
222
- this.log(`🔍 Authentication/Authorization Error:`);
223
- this.log(`🔍 - Token may be invalid or lack required permissions`);
224
- this.log(`🔍 - Required permission: 'actions: read' or 'repo' scope`);
225
- }
226
-
227
- if (data && data.message) {
228
- this.log(`🔍 Error message: ${data.message}`);
229
- }
230
- } else {
231
- this.log(`🔍 Network error or no response received`);
232
- }
233
- return null;
234
- }
235
- }
236
68
  async onEnd() {
237
69
  const { okToProceed, message } = this.preChecks();
238
70
  if (!okToProceed) {
239
71
  this.log(message);
240
72
  return;
241
73
  }
242
- // SHARDING SUPPORT - Stop slack messages for non-zero index shard(s)
243
- // Only shard 0 (0-based) should post when aggregating results from multiple shards
244
-
245
- this.log('🔍 [Shard Detection] Starting shard detection logic...');
246
-
247
- // Log all relevant environment variables
248
- this.log(`🔍 [Shard Detection] Environment variables:`);
249
- this.log(`🔍 CI: ${process.env.CI !== undefined ? `"${process.env.CI}"` : 'undefined'}`);
250
- this.log(`🔍 GITHUB_ACTIONS: ${process.env.GITHUB_ACTIONS !== undefined ? `"${process.env.GITHUB_ACTIONS}"` : 'undefined'}`);
251
- this.log(`🔍 GITHUB_JOB: ${process.env.GITHUB_JOB !== undefined ? `"${process.env.GITHUB_JOB}"` : 'undefined'}`);
252
- this.log(`🔍 MATRIX_SHARD: ${process.env.MATRIX_SHARD !== undefined ? `"${process.env.MATRIX_SHARD}"` : 'undefined'}`);
253
- this.log(`🔍 MATRIX_INDEX: ${process.env.MATRIX_INDEX !== undefined ? `"${process.env.MATRIX_INDEX}"` : 'undefined'}`);
254
- this.log(`🔍 MATRIX_COUNT: ${process.env.MATRIX_COUNT !== undefined ? `"${process.env.MATRIX_COUNT}"` : 'undefined'}`);
255
-
256
- // Step 1: Try GitHub Actions API detection (most robust for GitHub Actions)
257
- let githubApiShardInfo = null;
258
- if (process.env.GITHUB_ACTIONS === 'true') {
259
- this.log(`🔍 [Shard Detection] Attempting GitHub Actions API detection...`);
260
- githubApiShardInfo = await this.detectGitHubActionsShardInfo();
261
- }
262
-
263
- // Step 2: Detect shard index from environment variables (priority)
264
- let currentShardIndex = process.env.MATRIX_SHARD !== undefined
265
- ? process.env.MATRIX_SHARD
266
- : (process.env.MATRIX_INDEX !== undefined ? process.env.MATRIX_INDEX : undefined);
267
-
268
- // Step 3: Use GitHub API detection if env vars not set
269
- if (currentShardIndex === undefined && githubApiShardInfo) {
270
- currentShardIndex = githubApiShardInfo.shardIndex.toString();
271
- this.log(`🔍 [Shard Detection] Using GitHub API detected shardIndex: "${currentShardIndex}"`);
272
- }
273
-
274
- this.log(`🔍 [Shard Detection] Initial currentShardIndex: ${currentShardIndex !== undefined ? `"${currentShardIndex}"` : 'undefined'}`);
275
-
276
- // Step 4: Get shard count from multiple sources (priority order)
277
- let totalShards;
278
- if (process.env.MATRIX_COUNT !== undefined) {
279
- totalShards = parseInt(process.env.MATRIX_COUNT, 10);
280
- this.log(`🔍 [Shard Detection] Using MATRIX_COUNT env var: ${totalShards}`);
281
- } else if (githubApiShardInfo && githubApiShardInfo.totalShards > 1) {
282
- totalShards = githubApiShardInfo.totalShards;
283
- this.log(`🔍 [Shard Detection] Using GitHub API detected totalShards: ${totalShards}`);
284
- } else {
285
- // Fallback to ResultsParser
286
- const parserTotalShards = this.resultsParser.totalShardCount;
287
- totalShards = parserTotalShards;
288
- this.log(`🔍 [Shard Detection] Using ResultsParser totalShards: ${totalShards}`);
289
- }
290
-
291
- // Get shard info from ResultsParser for fallback
292
- const parserShardIndex = this.resultsParser.shardIndex;
293
- const parserTotalShards = this.resultsParser.totalShardCount;
294
-
295
- this.log(`🔍 [Shard Detection] ResultsParser values:`);
296
- this.log(`🔍 parserShardIndex: ${parserShardIndex !== undefined ? parserShardIndex : 'undefined'}`);
297
- this.log(`🔍 parserTotalShards: ${parserTotalShards !== undefined ? parserTotalShards : 'undefined'}`);
298
-
299
- this.log(`🔍 [Shard Detection] Calculated totalShards: ${totalShards}`);
300
-
301
- // If we still don't have shard index, try to infer from parser or GitHub API
302
- if (currentShardIndex === undefined) {
303
- if (githubApiShardInfo && githubApiShardInfo.shardIndex !== undefined) {
304
- currentShardIndex = githubApiShardInfo.shardIndex.toString();
305
- this.log(`🔍 [Shard Detection] Using GitHub API shardIndex as fallback: "${currentShardIndex}"`);
306
- } else if (parserShardIndex !== undefined) {
307
- currentShardIndex = parserShardIndex.toString();
308
- this.log(`🔍 [Shard Detection] Using parserShardIndex as fallback: "${currentShardIndex}"`);
309
- }
310
- }
311
-
312
- // CRITICAL: In CI (especially GitHub Actions), if we can't detect shard info, block posting
313
- // This prevents duplicate messages when running with multiple shards
314
- if (process.env.CI && currentShardIndex === undefined) {
315
- if (process.env.GITHUB_ACTIONS === 'true') {
316
- this.log(`❌ [Shard Detection] BLOCKING: GitHub Actions detected but shard index cannot be determined.`);
317
- this.log(`❌ Attempted: GitHub API detection ${githubApiShardInfo ? 'succeeded' : 'failed'}, env vars not set`);
318
- this.log(`❌ This prevents duplicate messages when running with multiple shards.`);
319
- } else {
320
- this.log(`❌ [Shard Detection] BLOCKING: CI environment detected but shard info not set.`);
321
- this.log(`❌ MATRIX_COUNT: undefined, MATRIX_SHARD: undefined`);
322
- this.log(`❌ This prevents duplicate messages when running with multiple shards.`);
323
- }
324
- this.log(`❌ Skipping Slack report to prevent duplicate messages from all shards.`);
325
- this.log(`💡 Fix: Add these env vars to your workflow:`);
326
- this.log(`💡 env:`);
327
- this.log(`💡 MATRIX_SHARD: $\{\{ strategy.job-index \}\} # 0-based (only shard 0 posts)`);
328
- this.log(`💡 MATRIX_COUNT: $\{\{ strategy.job-total \}\}`);
329
- this.log(`💡 OR ensure SAFETYWINGTEST_GITHUB_TOKEN has workflow permissions for API detection`);
330
- return;
331
- }
332
-
333
- // Convert to number for comparison (default to 0 if undefined)
334
- const shardIndexNum = currentShardIndex !== undefined ? parseInt(currentShardIndex, 10) : 0;
335
-
336
- this.log(`🔍 [Shard Detection] Final values:`);
337
- this.log(`🔍 currentShardIndex (string): ${currentShardIndex !== undefined ? `"${currentShardIndex}"` : 'undefined'}`);
338
- this.log(`🔍 shardIndexNum (number): ${shardIndexNum}`);
339
- this.log(`🔍 totalShards: ${totalShards}`);
340
-
341
- // CRITICAL: For GitHub Actions with multiple shards, only shard 0 should post
342
- // If we're in CI with multiple shards and don't know which shard we are, block all posts
343
- // This prevents duplicate messages when MATRIX_SHARD/MATRIX_INDEX is not set
344
- if (process.env.CI && totalShards > 1 && currentShardIndex === undefined) {
345
- this.log(`❌ [Shard Detection] BLOCKING: CI environment detected with ${totalShards} shards but shard index not set (MATRIX_SHARD/MATRIX_INDEX missing).`);
346
- this.log(`❌ Skipping Slack report to prevent duplicate messages from all shards.`);
347
- this.log(`💡 Fix: Add these env vars to your workflow:`);
348
- this.log(`💡 env:`);
349
- this.log(`💡 MATRIX_SHARD: $\{\{ strategy.job-index \}\} # 0-based (only shard 0 posts)`);
350
- this.log(`💡 MATRIX_COUNT: $\{\{ strategy.job-total \}\}`);
351
- return;
352
- }
353
-
354
- // Only allow shard 0 (0-based) to post when there are multiple shards
355
- // This ensures only one shard posts the aggregated report
356
- if (totalShards > 1 && shardIndexNum !== 0) {
357
- this.log(`❌ [Shard Detection] BLOCKING: Non-zero shard detected (shard ${currentShardIndex} of ${totalShards})`);
358
- this.log(`❌ Stopping reporter for non-zero index shard ${currentShardIndex} of ${totalShards}`);
359
- this.log(`ℹ️ Only shard 0 will post the aggregated report.`);
360
- return;
361
- }
362
-
363
- // Single shard - always allow posting
364
- if (totalShards === 1) {
365
- this.log(`✅ [Shard Detection] ALLOWING: Single shard detected. Posting Slack report.`);
366
- } else {
367
- this.log(`✅ [Shard Detection] ALLOWING: Multiple shards detected (${totalShards}). Shard ${currentShardIndex} (index ${shardIndexNum}) will post aggregated report.`);
368
- }
369
-
370
74
  const resultSummary = await this.resultsParser.getParsedResults();
75
+ // SHARDING SUPPORT - Stop slack messages for non-shard-1 shard(s)
76
+ if (process.env.SHARD_INDEX && process.env.SHARD_INDEX !== '1') {
77
+ this.log(`❌ Stopping reporter for non-shard-1 index ${process.env.SHARD_INDEX} of ${process.env.TOTAL_SHARDS}`);
78
+ return;
79
+ }
371
80
  resultSummary.meta = this.meta;
372
81
  const maxRetry = Math.max(...resultSummary.tests.map((o) => o.retry));
373
82
  if (this.sendResults === 'on-failure'
package/package.json CHANGED
@@ -4,7 +4,8 @@
4
4
  "@slack/webhook": "^6.1.0",
5
5
  "https-proxy-agent": "^7.0.1",
6
6
  "adm-zip": "^0.5.10",
7
- "axios": "^1.0.0"
7
+ "axios": "^1.0.0",
8
+ "jsonwebtoken": "^9.0.0"
8
9
  },
9
10
  "devDependencies": {
10
11
  "@playwright/test": "^1.23.3",
@@ -32,7 +33,7 @@
32
33
  "lint-fix": "npx eslint . --ext .ts --fix"
33
34
  },
34
35
  "name": "playwright-slack-report-burak",
35
- "version": "3.0.112",
36
+ "version": "3.1.0",
36
37
  "main": "index.js",
37
38
  "types": "dist/index.d.ts",
38
39
  "author": "Burak B. <burak.boluk@hotmail.com>",