playwright-slack-report-burak 2.4.3 → 3.0.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,6 +8,24 @@ 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.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
+
11
29
  const generateAllRunSuites = async (summaryResults) => {
12
30
  const suitesResults = [];
13
31
  const allSuites = [];
@@ -255,12 +273,13 @@ const generateProblemCaseList = async (summaryResults) => {
255
273
  let text = `${title}${list.map((value, index) => `*${index + 1}.* ${value}`).join('\n')}`;
256
274
  if (text.length > 2700) {
257
275
  text = text.substring(0, 2700) + '...';
276
+ const buildInfo = getBuildInfo();
258
277
  array.push({
259
278
  type: 'section',
260
279
  text: {
261
280
  type: 'mrkdwn',
262
- text: text + '\n\n\n' + '⚠️ *There are too many items to display here. ' +
263
- 'You can view more on CircleCI* ⚠️',
281
+ text: text + '\n\n\n' + `⚠️ *There are too many items to display here. ` +
282
+ `You can view more on ${buildInfo.platform}* ⚠️`,
264
283
  },
265
284
  },
266
285
  {
@@ -271,7 +290,7 @@ const generateProblemCaseList = async (summaryResults) => {
271
290
  type: 'plain_text',
272
291
  text: 'Go to Build Details'
273
292
  },
274
- url: process.env.CIRCLE_BUILD_URL
293
+ url: buildInfo.url
275
294
  }]
276
295
  });
277
296
  } else {
@@ -356,12 +375,13 @@ const generateFailuresReasons = async (summaryResults) => {
356
375
  },
357
376
  });
358
377
  if (i === summaryResults.failures.length-1) {
378
+ const buildInfo = getBuildInfo();
359
379
  failsList.push({
360
380
  type: 'section',
361
381
  text: {
362
382
  type: 'mrkdwn',
363
383
  text: '⚠️ *There are too many failures to display in full detail. '+
364
- 'You can view more on CircleCI* ⚠️',
384
+ `You can view more on ${buildInfo.platform}* ⚠️`,
365
385
  },
366
386
  },
367
387
  {
@@ -373,7 +393,7 @@ const generateFailuresReasons = async (summaryResults) => {
373
393
  type: 'plain_text',
374
394
  text: 'Go to Build Details'
375
395
  },
376
- url: process.env.CIRCLE_BUILD_URL
396
+ url: buildInfo.url
377
397
  }
378
398
  ]
379
399
  });
@@ -391,12 +411,13 @@ const generateFailuresReasons = async (summaryResults) => {
391
411
  },
392
412
  });
393
413
  if (i > 23) {
414
+ const buildInfo = getBuildInfo();
394
415
  failsList.push({
395
416
  type: 'section',
396
417
  text: {
397
418
  type: 'mrkdwn',
398
419
  text: '⚠️ *There are too many failures to display here '+
399
- 'You can view more on CircleCI* ⚠️',
420
+ `You can view more on ${buildInfo.platform}* ⚠️`,
400
421
  },
401
422
  },
402
423
  {
@@ -407,7 +428,7 @@ const generateFailuresReasons = async (summaryResults) => {
407
428
  type: 'plain_text',
408
429
  text: 'Go to Build Details'
409
430
  },
410
- url: process.env.CIRCLE_BUILD_URL
431
+ url: buildInfo.url
411
432
  }]
412
433
  });
413
434
  break;
@@ -10,12 +10,27 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const axios = require('axios');
12
12
  const { exec } = require('child_process');
13
+ let AdmZip;
14
+ try {
15
+ AdmZip = require('adm-zip');
16
+ } catch (e) {
17
+ // adm-zip is required for GitHub Actions artifact extraction
18
+ AdmZip = null;
19
+ }
13
20
 
14
21
  class ResultsParser {
15
22
  result;
16
23
  separateFlakyTests;
17
- totalShardCount = process.env.CIRCLE_NODE_TOTAL ? parseInt(process.env.CIRCLE_NODE_TOTAL, 10) : 1;
18
- shardIndex = process.env.CIRCLE_NODE_INDEX ? parseInt(process.env.CIRCLE_NODE_INDEX, 10) : 0;
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);
30
+
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;
19
34
  constructor(options = { separateFlakyTests: false }) {
20
35
  this.result = [];
21
36
  this.separateFlakyTests = options.separateFlakyTests;
@@ -77,7 +92,7 @@ class ResultsParser {
77
92
 
78
93
  if (this.shardIndex === 0 && this.totalShardCount > 1) {
79
94
  if (process.env.CI) {
80
- console.log('Fetching all artifacts...');
95
+ console.log('Fetching all artifacts from GitHub Actions...');
81
96
  await this.fetchAllArtifacts();
82
97
  } else {
83
98
  while (!this.allNodeSummaryFilesExist() || !this.allBlobZipsExist()) {
@@ -333,6 +348,10 @@ class ResultsParser {
333
348
  if (!fs.existsSync(summariesDir)) {
334
349
  fs.mkdirSync(summariesDir, { recursive: true });
335
350
  }
351
+ // Fetch artifacts for shards 1 to totalShardCount-1 (if using 1-based) or 0 to totalShardCount-1 (if 0-based)
352
+ // Since we're shard 0, we need to fetch from other shards
353
+ // The workflow uses 1-based shards (1, 2, 3...), but we need to map them to 0-based internally (0, 1, 2...)
354
+ // So shard 1 in workflow = shard 0 internally, shard 2 = shard 1, etc.
336
355
  for (let i = 1; i < this.totalShardCount; i++) {
337
356
  await this.fetchArtifact(i, `node_summary_${i}.json`, summariesDir);
338
357
  await this.fetchArtifact(i, `blob-report-node-${i}.zip`, summariesDir);
@@ -341,30 +360,216 @@ class ResultsParser {
341
360
  }
342
361
 
343
362
  async fetchArtifact(i, file, summariesDir) {
344
- const circleciToken = process.env.safetywingtest_CIRCLECI_API_TOKEN;
345
- const circleciJobId = process.env.CIRCLE_WORKFLOW_JOB_ID || '8133c154-ceb3-466c-b4e1-d3d8768b60fa';
346
- const circleciApiUrl = `https://output.circle-artifacts.com/output/job/${circleciJobId}/artifacts/${i}/html-report/${file}`;
347
363
  const filePath = path.join(summariesDir, file);
364
+ await this.fetchArtifactFromGitHubActions(i, file, filePath);
365
+ }
366
+
367
+ // Helper to extract shard index from artifact name
368
+ extractShardIndexFromArtifactName(artifactName, i) {
369
+ // Try to extract number from artifact name (e.g., html-report-1 -> 1)
370
+ const match = artifactName.match(/-(\d+)$/);
371
+ if (match) {
372
+ const artifactShard = parseInt(match[1], 10);
373
+ // If artifact uses 1-based indexing, convert to 0-based for internal use
374
+ // html-report-1 (workflow) -> shard 0 (internal)
375
+ // html-report-2 (workflow) -> shard 1 (internal)
376
+ return artifactShard - 1;
377
+ }
378
+ // Fallback to the provided index
379
+ return i;
380
+ }
381
+
382
+ async fetchArtifactFromGitHubActions(i, file, filePath) {
383
+ const githubToken = process.env.GITHUB_TOKEN;
384
+ const repo = process.env.GITHUB_REPOSITORY;
385
+ const runId = process.env.GITHUB_RUN_ID;
386
+
387
+ if (!githubToken || !repo) {
388
+ console.error('GitHub Actions: Missing GITHUB_TOKEN or GITHUB_REPOSITORY');
389
+ return;
390
+ }
348
391
 
392
+ // GitHub Actions artifact API endpoint
393
+ const githubApiUrl = `https://api.github.com/repos/${repo}/actions/artifacts`;
394
+
395
+ // Try multiple artifact naming patterns:
396
+ // 1. html-report-{i} (1-based workflow style)
397
+ // 2. blob-report-node-{i} (direct blob reports)
398
+ // 3. test-results-{i} (alternative naming)
399
+ // Note: i is 0-based internally, so for 1-based workflows we need i+1
400
+ const artifactNamePatterns = [
401
+ `html-report-${i + 1}`, // 1-based workflow style (most common)
402
+ `blob-report-node-${i}`, // 0-based direct blob
403
+ `blob-report-node-${i + 1}`, // 1-based direct blob
404
+ `html-report-${i}`, // 0-based html-report
405
+ `test-results-${i + 1}`, // Alternative naming
406
+ ];
407
+
349
408
  while (true) {
350
409
  try {
351
- const response = await axios.get(circleciApiUrl, {
410
+ // List all artifacts to find the one we need
411
+ const listResponse = await axios.get(githubApiUrl, {
352
412
  headers: {
353
- 'Circle-Token': circleciToken,
413
+ 'Authorization': `token ${githubToken}`,
414
+ 'Accept': 'application/vnd.github.v3+json'
354
415
  },
355
- responseType: 'arraybuffer'
416
+ params: {
417
+ per_page: 100
418
+ }
356
419
  });
357
-
358
- fs.writeFileSync(filePath, response.data);
359
- console.log(`Successfully fetched file ${file} from shard ${i}`);
360
- break; // Exit the loop if the file is fetched successfully
420
+
421
+ // Try to find artifact matching any of our patterns
422
+ let artifact = null;
423
+ for (const pattern of artifactNamePatterns) {
424
+ artifact = listResponse.data.artifacts.find(
425
+ (a) => a.name === pattern ||
426
+ a.name.includes(`shard-${i}`) ||
427
+ a.name.includes(`shard-${i + 1}`) ||
428
+ a.name.includes(`node-${i}`) ||
429
+ a.name.includes(`node-${i + 1}`)
430
+ );
431
+ if (artifact) {
432
+ console.log(`Found artifact: ${artifact.name} (matched pattern: ${pattern})`);
433
+ break;
434
+ }
435
+ }
436
+
437
+ if (artifact && artifact.archive_download_url) {
438
+ // Download the artifact ZIP
439
+ const downloadResponse = await axios.get(artifact.archive_download_url, {
440
+ headers: {
441
+ 'Authorization': `token ${githubToken}`,
442
+ 'Accept': 'application/vnd.github.v3+json'
443
+ },
444
+ responseType: 'arraybuffer'
445
+ });
446
+
447
+ if (!AdmZip) {
448
+ console.error('adm-zip is required for GitHub Actions artifact extraction. Please install it: npm install adm-zip');
449
+ return;
450
+ }
451
+
452
+ // Extract ZIP and find the specific file
453
+ const zip = new AdmZip(downloadResponse.data);
454
+
455
+ // Try direct file match first
456
+ let zipEntry = zip.getEntry(file);
457
+
458
+ // If not found, try common path variations
459
+ if (!zipEntry) {
460
+ const possiblePaths = [
461
+ file,
462
+ `playwright-report/${file}`,
463
+ `playwright-report/${file}`,
464
+ `html-report-${i + 1}/${file}`,
465
+ `html-report-${i}/${file}`,
466
+ ];
467
+ for (const possiblePath of possiblePaths) {
468
+ zipEntry = zip.getEntry(possiblePath);
469
+ if (zipEntry) break;
470
+ }
471
+ }
472
+
473
+ if (zipEntry) {
474
+ fs.writeFileSync(filePath, zipEntry.getData());
475
+ console.log(`Successfully fetched file ${file} from GitHub Actions shard ${i} (artifact: ${artifact.name})`);
476
+ break;
477
+ } else {
478
+ // If file not found in ZIP root, extract and search recursively
479
+ const extractPath = path.join(path.dirname(filePath), `temp-artifact-${i}`);
480
+ zip.extractAllTo(extractPath, true);
481
+
482
+ // Extract shard index from artifact name to handle files named with index 0
483
+ const artifactShardIndex = this.extractShardIndexFromArtifactName(artifact.name, i);
484
+
485
+ // Look for the file recursively, also try with shard index from artifact name
486
+ const searchInDir = (dir, targetFile, alternateTargetFile = null) => {
487
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
488
+ for (const entry of entries) {
489
+ const fullPath = path.join(dir, entry.name);
490
+ if (entry.isDirectory()) {
491
+ const found = searchInDir(fullPath, targetFile, alternateTargetFile);
492
+ if (found) return found;
493
+ } else {
494
+ // Check exact match
495
+ if (entry.name === targetFile) {
496
+ return fullPath;
497
+ }
498
+ // Check alternate filename (e.g., node_summary_0.json when we need node_summary_1.json)
499
+ if (alternateTargetFile && entry.name === alternateTargetFile) {
500
+ return fullPath;
501
+ }
502
+ // Check if ends with target file
503
+ if (entry.name.endsWith(targetFile)) {
504
+ return fullPath;
505
+ }
506
+ }
507
+ }
508
+ return null;
509
+ };
510
+
511
+ // Try to find the file, also try with shard index from artifact
512
+ let foundPath = searchInDir(extractPath, file);
513
+
514
+ // If file uses index 0 pattern (like node_summary_0.json or blob-report-node-0.zip)
515
+ // but we need it for a different shard, try to find any matching pattern
516
+ if (!foundPath && (file.includes('_0') || file.includes('-node-0'))) {
517
+ // Recursively search for any node_summary or blob-report file
518
+ const findAllFiles = (dir, pattern) => {
519
+ const results = [];
520
+ try {
521
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
522
+ for (const entry of entries) {
523
+ const fullPath = path.join(dir, entry.name);
524
+ if (entry.isDirectory()) {
525
+ results.push(...findAllFiles(fullPath, pattern));
526
+ } else if (entry.name.includes(pattern)) {
527
+ results.push(fullPath);
528
+ }
529
+ }
530
+ } catch (e) {
531
+ // Ignore errors
532
+ }
533
+ return results;
534
+ };
535
+
536
+ // Find any node_summary or blob-report files
537
+ const matchingFiles = findAllFiles(extractPath, file.includes('node_summary') ? 'node_summary' : 'blob-report');
538
+
539
+ if (matchingFiles.length > 0) {
540
+ foundPath = matchingFiles[0];
541
+ // Copy to the expected path with correct shard index
542
+ fs.copyFileSync(foundPath, filePath);
543
+ console.log(`Found and copied file ${path.basename(foundPath)} -> ${file} from artifact ${artifact.name}`);
544
+ // Clean up temp directory
545
+ fs.rmSync(extractPath, { recursive: true, force: true });
546
+ break;
547
+ }
548
+ }
549
+
550
+ if (foundPath) {
551
+ fs.copyFileSync(foundPath, filePath);
552
+ console.log(`Successfully fetched file ${file} from GitHub Actions shard ${i} (artifact: ${artifact.name})`);
553
+ // Clean up temp directory
554
+ fs.rmSync(extractPath, { recursive: true, force: true });
555
+ break;
556
+ } else {
557
+ console.warn(`File ${file} not found in artifact ${artifact.name}. Retrying in 10 seconds...`);
558
+ fs.rmSync(extractPath, { recursive: true, force: true });
559
+ await new Promise(resolve => setTimeout(resolve, 10000));
560
+ }
561
+ }
562
+ } else {
563
+ console.warn(`Artifact not found (tried: ${artifactNamePatterns.join(', ')}). Retrying in 10 seconds...`);
564
+ await new Promise(resolve => setTimeout(resolve, 10000));
565
+ }
361
566
  } catch (error) {
362
567
  if (error.response && error.response.status === 404) {
363
- console.warn(`File ${file} not found at ${circleciApiUrl}. Retrying in 10 seconds...`);
364
- await new Promise(resolve => setTimeout(resolve, 10000)); // Wait for 10 seconds
568
+ console.warn(`Artifact not found. Retrying in 10 seconds...`);
569
+ await new Promise(resolve => setTimeout(resolve, 10000));
365
570
  } else {
366
- console.error(`Failed to fetch file ${file} from shard ${i}!`, error);
367
- break; // Exit the loop on other errors
571
+ console.error(`Failed to fetch artifact from GitHub Actions shard ${i}!`, error.message);
572
+ await new Promise(resolve => setTimeout(resolve, 10000));
368
573
  }
369
574
  }
370
575
  }
@@ -71,9 +71,18 @@ class SlackReporter {
71
71
  return;
72
72
  }
73
73
  const resultSummary = await this.resultsParser.getParsedResults();
74
- // SHARDING SUPPORT - Stop slack messages for non-zero index node(s)
75
- if (process.env.CIRCLE_NODE_INDEX && process.env.CIRCLE_NODE_INDEX !== '0') {
76
- this.log(`❌ Stopping reporter for non-zero index node ${process.env.CIRCLE_NODE_INDEX} of ${process.env.CIRCLE_NODE_TOTAL}`);
74
+ // SHARDING SUPPORT - Stop slack messages for non-zero index shard(s)
75
+ // Support both explicit MATRIX_SHARD/MATRIX_INDEX and workflow-style 1-based indexing
76
+ // If MATRIX_SHARD is not set, shard 0 will still send reports (handles single shard case)
77
+ const currentShardIndex = process.env.MATRIX_SHARD !== undefined
78
+ ? process.env.MATRIX_SHARD
79
+ : (process.env.MATRIX_INDEX !== undefined ? process.env.MATRIX_INDEX : undefined);
80
+
81
+ // Only skip if explicitly set and not 0 (or '0')
82
+ // This allows shard 0 to send reports even when MATRIX_SHARD is not set
83
+ if (currentShardIndex !== undefined && currentShardIndex !== '0' && currentShardIndex !== 0) {
84
+ const totalShards = process.env.MATRIX_COUNT || '1';
85
+ this.log(`❌ Stopping reporter for non-zero index shard ${currentShardIndex} of ${totalShards}`);
77
86
  return;
78
87
  }
79
88
  resultSummary.meta = this.meta;
package/package.json CHANGED
@@ -2,7 +2,8 @@
2
2
  "dependencies": {
3
3
  "@slack/web-api": "^6.8.1",
4
4
  "@slack/webhook": "^6.1.0",
5
- "https-proxy-agent": "^7.0.1"
5
+ "https-proxy-agent": "^7.0.1",
6
+ "adm-zip": "^0.5.10"
6
7
  },
7
8
  "devDependencies": {
8
9
  "@playwright/test": "^1.23.3",
@@ -30,7 +31,7 @@
30
31
  "lint-fix": "npx eslint . --ext .ts --fix"
31
32
  },
32
33
  "name": "playwright-slack-report-burak",
33
- "version": "2.4.3",
34
+ "version": "3.0.0",
34
35
  "main": "index.js",
35
36
  "types": "dist/index.d.ts",
36
37
  "author": "Burak B. <burak.boluk@hotmail.com>",