playwright-slack-report-burak 3.0.20 → 3.0.22

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.
@@ -33,16 +33,12 @@ try {
33
33
  class ResultsParser {
34
34
  result;
35
35
  separateFlakyTests;
36
- // Handle both 0-based and 1-based shard indexing
37
- // If MATRIX_SHARD is not set, try to infer from workflow (1-based) or default to 0
38
36
  totalShardCount = process.env.TOTAL_SHARDS
39
37
  ? parseInt(process.env.TOTAL_SHARDS, 10)
40
- : (process.env.MATRIX_COUNT ? parseInt(process.env.MATRIX_COUNT, 10) : 1);
38
+ : 1;
41
39
  shardIndex = process.env.SHARD_INDEX !== undefined
42
40
  ? parseInt(process.env.SHARD_INDEX, 10)
43
- : (process.env.MATRIX_SHARD !== undefined
44
- ? parseInt(process.env.MATRIX_SHARD, 10)
45
- : (process.env.MATRIX_INDEX !== undefined ? parseInt(process.env.MATRIX_INDEX, 10) : 0));
41
+ : 1;
46
42
 
47
43
  constructor(options = { separateFlakyTests: false }) {
48
44
  this.result = [];
@@ -438,17 +434,14 @@ class ResultsParser {
438
434
  // GitHub Actions artifact API endpoint
439
435
  const githubApiUrl = `https://api.github.com/repos/${repo}/actions/artifacts`;
440
436
 
441
- // Try multiple artifact naming patterns:
442
- // 1. html-report-{i} (1-based workflow style, i is already 1-based)
443
- // 2. blob-report-node-{i} (direct blob reports, i is 1-based)
444
- // 3. test-results-{i} (alternative naming, i is 1-based)
445
- // Note: i is 1-based internally now, so we use it directly for GitHub Actions artifacts
437
+ // Try multiple artifact naming patterns (all 1-based):
438
+ // 1. html-report-{i} (1-based workflow style)
439
+ // 2. blob-report-node-{i} (direct blob reports)
440
+ // 3. test-results-{i} (alternative naming)
446
441
  const artifactNamePatterns = [
447
- `html-report-${i}`, // 1-based workflow style (i is already 1-based)
448
- `blob-report-node-${i}`, // 1-based direct blob
449
- `html-report-${i - 1}`, // 0-based html-report (fallback)
450
- `blob-report-node-${i - 1}`, // 0-based direct blob (fallback)
451
- `test-results-${i}`, // Alternative naming
442
+ `html-report-${i}`,
443
+ `blob-report-node-${i}`,
444
+ `test-results-${i}`,
452
445
  ];
453
446
 
454
447
  while (true) {
@@ -469,10 +462,8 @@ class ResultsParser {
469
462
  for (const pattern of artifactNamePatterns) {
470
463
  artifact = listResponse.data.artifacts.find(
471
464
  (a) => a.name === pattern ||
472
- a.name.includes(`shard-${i}`) ||
473
- a.name.includes(`shard-${i - 1}`) ||
474
- a.name.includes(`node-${i}`) ||
475
- a.name.includes(`node-${i - 1}`)
465
+ a.name.includes(`shard-${i}`) ||
466
+ a.name.includes(`node-${i}`)
476
467
  );
477
468
  if (artifact) {
478
469
  console.log(`Found artifact: ${artifact.name} (matched pattern: ${pattern})`);
@@ -506,9 +497,8 @@ class ResultsParser {
506
497
  const possiblePaths = [
507
498
  file,
508
499
  `playwright-report/${file}`,
509
- `playwright-report/${file}`,
510
500
  `html-report-${i}/${file}`,
511
- `html-report-${i - 1}/${file}`,
501
+ `html-report-${i}/playwright-report/${file}`,
512
502
  ];
513
503
  for (const possiblePath of possiblePaths) {
514
504
  zipEntry = zip.getEntry(possiblePath);
@@ -526,22 +516,18 @@ class ResultsParser {
526
516
  zip.extractAllTo(extractPath, true);
527
517
 
528
518
  // Look for the file recursively
529
- const searchInDir = (dir, targetFile, alternateTargetFile = null) => {
519
+ const searchInDir = (dir, targetFile) => {
530
520
  const entries = fs.readdirSync(dir, { withFileTypes: true });
531
521
  for (const entry of entries) {
532
522
  const fullPath = path.join(dir, entry.name);
533
523
  if (entry.isDirectory()) {
534
- const found = searchInDir(fullPath, targetFile, alternateTargetFile);
524
+ const found = searchInDir(fullPath, targetFile);
535
525
  if (found) return found;
536
526
  } else {
537
527
  // Check exact match
538
528
  if (entry.name === targetFile) {
539
529
  return fullPath;
540
530
  }
541
- // Check alternate filename (e.g., node_summary_0.json when we need node_summary_1.json)
542
- if (alternateTargetFile && entry.name === alternateTargetFile) {
543
- return fullPath;
544
- }
545
531
  // Check if ends with target file
546
532
  if (entry.name.endsWith(targetFile)) {
547
533
  return fullPath;
@@ -551,45 +537,9 @@ class ResultsParser {
551
537
  return null;
552
538
  };
553
539
 
554
- // Try to find the file, also try with shard index from artifact
540
+ // Try to find the file recursively
555
541
  let foundPath = searchInDir(extractPath, file);
556
542
 
557
- // If file uses index 0 pattern (like node_summary_0.json or blob-report-node-0.zip)
558
- // but we need it for a different shard, try to find any matching pattern
559
- if (!foundPath && (file.includes('_0') || file.includes('-node-0'))) {
560
- // Recursively search for any node_summary or blob-report file
561
- const findAllFiles = (dir, pattern) => {
562
- const results = [];
563
- try {
564
- const entries = fs.readdirSync(dir, { withFileTypes: true });
565
- for (const entry of entries) {
566
- const fullPath = path.join(dir, entry.name);
567
- if (entry.isDirectory()) {
568
- results.push(...findAllFiles(fullPath, pattern));
569
- } else if (entry.name.includes(pattern)) {
570
- results.push(fullPath);
571
- }
572
- }
573
- } catch (e) {
574
- // Ignore errors
575
- }
576
- return results;
577
- };
578
-
579
- // Find any node_summary or blob-report files
580
- const matchingFiles = findAllFiles(extractPath, file.includes('node_summary') ? 'node_summary' : 'blob-report');
581
-
582
- if (matchingFiles.length > 0) {
583
- foundPath = matchingFiles[0];
584
- // Copy to the expected path with correct shard index
585
- fs.copyFileSync(foundPath, filePath);
586
- console.log(`Found and copied file ${path.basename(foundPath)} -> ${file} from artifact ${artifact.name}`);
587
- // Clean up temp directory
588
- fs.rmSync(extractPath, { recursive: true, force: true });
589
- break;
590
- }
591
- }
592
-
593
543
  if (foundPath) {
594
544
  fs.copyFileSync(foundPath, filePath);
595
545
  console.log(`Successfully fetched file ${file} from GitHub Actions shard ${i} (artifact: ${artifact.name})`);
@@ -90,240 +90,6 @@ class SlackReporter {
90
90
  onTestEnd(test, result) {
91
91
  this.resultsParser.addTestResult(test.parent.title, test, this.browsers);
92
92
  }
93
- /**
94
- * Detect GitHub Actions shard information using GitHub API
95
- * Returns { shardIndex: number, totalShards: number } or null if detection fails
96
- */
97
- async detectGitHubActionsShardInfo() {
98
- // Check if we're in GitHub Actions
99
- if (!process.env.GITHUB_ACTIONS || process.env.GITHUB_ACTIONS !== 'true') {
100
- return null;
101
- }
102
- // Try GITHUB_TOKEN first (automatic), then fallback to custom token
103
- const githubToken = process.env.GITHUB_TOKEN || process.env.SAFETYWINGTEST_GITHUB_TOKEN;
104
- const repo = process.env.GITHUB_REPOSITORY;
105
- const runId = process.env.GITHUB_RUN_ID;
106
- const currentJobId = process.env.GITHUB_JOB;
107
- if (!githubToken || !repo || !runId) {
108
- 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}`);
109
- return null;
110
- }
111
- this.log(`🔍 [GitHub Actions Detection] Using token: ${process.env.GITHUB_TOKEN ? 'GITHUB_TOKEN' : 'SAFETYWINGTEST_GITHUB_TOKEN'}`);
112
- this.log(`🔍 [GitHub Actions Detection] Repository: ${repo}, Run ID: ${runId}`);
113
- if (!axios) {
114
- this.log(`🔍 [GitHub Actions Detection] axios not available, skipping API detection`);
115
- return null;
116
- }
117
- try {
118
- // Get workflow run details
119
- const apiUrl = `https://api.github.com/repos/${repo}/actions/runs/${runId}/jobs`;
120
- this.log(`🔍 [GitHub Actions Detection] API URL: ${apiUrl}`);
121
- const response = await axios.get(apiUrl, {
122
- headers: {
123
- 'Accept': 'application/vnd.github+json',
124
- 'Authorization': `Bearer ${githubToken}`,
125
- 'X-GitHub-Api-Version': '2022-11-28'
126
- },
127
- params: {
128
- per_page: 100
129
- }
130
- });
131
- const jobs = response.data.jobs || [];
132
- if (jobs.length === 0) {
133
- this.log(`🔍 [GitHub Actions Detection] No jobs found in workflow run`);
134
- return null;
135
- }
136
-
137
- this.log(`🔍 [GitHub Actions Detection] Found ${jobs.length} total jobs in workflow run`);
138
-
139
- // Helper function to normalize job names by removing shard indicators
140
- // e.g., "Run tests (shard 3/4)" -> "Run tests"
141
- const normalizeJobName = (name) => {
142
- if (!name) return '';
143
- // Remove common matrix patterns: (shard X/Y), (X, Y), [X], etc.
144
- return name
145
- .replace(/\s*\(shard\s+\d+\/\d+\)/gi, '')
146
- .replace(/\s*\(\d+\/\d+\)/g, '')
147
- .replace(/\s*\(\d+,\s*\d+\)/g, '')
148
- .replace(/\s*\[\d+\]/g, '')
149
- .trim();
150
- };
151
-
152
- // Group jobs by normalized name to find matrix jobs
153
- const jobGroups = {};
154
- for (const job of jobs) {
155
- const jobName = job.name || '';
156
- const normalizedName = normalizeJobName(jobName);
157
- if (!jobGroups[normalizedName]) {
158
- jobGroups[normalizedName] = [];
159
- }
160
- jobGroups[normalizedName].push(job);
161
- }
162
-
163
- this.log(`🔍 [GitHub Actions Detection] Grouped into ${Object.keys(jobGroups).length} job group(s)`);
164
- for (const [groupName, groupJobs] of Object.entries(jobGroups)) {
165
- this.log(`🔍 Group "${groupName}": ${groupJobs.length} job(s)`);
166
- }
167
-
168
- // Find the job group that contains the current job
169
- let currentJob = null;
170
- let jobGroup = null;
171
-
172
- // Strategy 1: Find the currently running job (status: in_progress or completed recently)
173
- const runningJobs = jobs.filter(j => j.status === 'in_progress' || j.status === 'queued');
174
- this.log(`🔍 [GitHub Actions Detection] Found ${runningJobs.length} running/queued job(s)`);
175
-
176
- if (runningJobs.length === 1) {
177
- // Perfect - only one job is running, must be us
178
- currentJob = runningJobs[0];
179
- const normalizedName = normalizeJobName(currentJob.name);
180
- jobGroup = jobGroups[normalizedName];
181
- this.log(`🔍 [GitHub Actions Detection] Matched by running status: "${currentJob.name}"`);
182
- } else if (runningJobs.length > 1) {
183
- // Multiple running jobs - try to match by job ID or name
184
- for (const job of runningJobs) {
185
- // Check if this job's name contains the GITHUB_JOB id
186
- // But be more specific - avoid matching "Generate test matrix" when looking for "test"
187
- const jobIdLower = currentJobId.toLowerCase();
188
- const jobNameLower = (job.name || '').toLowerCase();
189
-
190
- // Match if job name starts with or equals the job ID (more specific)
191
- if (jobNameLower.startsWith(jobIdLower) ||
192
- jobNameLower.includes(`${jobIdLower} `) ||
193
- jobNameLower.includes(`${jobIdLower}(`) ||
194
- jobNameLower === jobIdLower) {
195
- currentJob = job;
196
- const normalizedName = normalizeJobName(job.name);
197
- jobGroup = jobGroups[normalizedName];
198
- this.log(`🔍 [GitHub Actions Detection] Matched running job by ID match: "${job.name}"`);
199
- break;
200
- }
201
- }
202
-
203
- // Still no match? Take the first running job that looks like a test job
204
- if (!currentJob) {
205
- for (const job of runningJobs) {
206
- if (job.name && (job.name.includes('shard') || job.name.includes('test') || job.name.includes('matrix'))) {
207
- currentJob = job;
208
- const normalizedName = normalizeJobName(job.name);
209
- jobGroup = jobGroups[normalizedName];
210
- this.log(`🔍 [GitHub Actions Detection] Matched by test-like name: "${job.name}"`);
211
- break;
212
- }
213
- }
214
- }
215
- }
216
-
217
- // Strategy 2: If no running job found, look at all jobs
218
- if (!currentJob) {
219
- this.log(`🔍 [GitHub Actions Detection] No running job matched, checking all jobs`);
220
- for (const job of jobs) {
221
- const jobIdLower = currentJobId.toLowerCase();
222
- const jobNameLower = (job.name || '').toLowerCase();
223
-
224
- if (jobNameLower.startsWith(jobIdLower) ||
225
- jobNameLower.includes(`${jobIdLower} `) ||
226
- jobNameLower.includes(`${jobIdLower}(`)) {
227
- currentJob = job;
228
- const normalizedName = normalizeJobName(job.name);
229
- jobGroup = jobGroups[normalizedName];
230
- this.log(`🔍 [GitHub Actions Detection] Matched job by name: "${job.name}"`);
231
- break;
232
- }
233
- }
234
- }
235
-
236
- // Fallback: Use the largest job group (likely the matrix jobs)
237
- if (!jobGroup || jobGroup.length === 0) {
238
- this.log(`🔍 [GitHub Actions Detection] Could not match job, using largest job group`);
239
- let largestGroup = [];
240
- let largestGroupName = '';
241
- for (const [groupName, groupJobs] of Object.entries(jobGroups)) {
242
- if (groupJobs.length > largestGroup.length) {
243
- largestGroup = groupJobs;
244
- largestGroupName = groupName;
245
- }
246
- }
247
- jobGroup = largestGroup;
248
- currentJob = jobGroup[0];
249
- this.log(`🔍 [GitHub Actions Detection] Using largest group "${largestGroupName}" with ${jobGroup.length} job(s)`);
250
- }
251
- // Sort jobs by name or ID to get consistent ordering
252
- jobGroup.sort((a, b) => {
253
- if (a.name && b.name) {
254
- return a.name.localeCompare(b.name);
255
- }
256
- return a.id - b.id;
257
- });
258
-
259
- // Find index of current job in the sorted group
260
- const currentJobIndex = jobGroup.findIndex(j => j.id === currentJob?.id || j.name === currentJob?.name);
261
-
262
- // Try to extract shard number from job name (e.g., "shard 3/4" -> index 3)
263
- // GitHub Actions uses 1-based indexing (shard 1, 2, 3, 4), we keep it 1-based internally for shard 1 logic
264
- let shardIndex = currentJobIndex >= 0 ? currentJobIndex : 0;
265
- if (currentJob && currentJob.name) {
266
- const shardMatch = currentJob.name.match(/shard\s+(\d+)\/(\d+)/i);
267
- if (shardMatch) {
268
- const shardNum = parseInt(shardMatch[1], 10); // 1-based from GitHub Actions (1, 2, 3, 4)
269
- const totalFromName = parseInt(shardMatch[2], 10);
270
- // Keep GitHub Actions 1-based indexing (shard 1 -> index 1, shard 2 -> index 2, etc.)
271
- // This allows shard 1 to be the aggregator
272
- shardIndex = shardNum;
273
- this.log(`🔍 [GitHub Actions Detection] Extracted shard info from name: ${shardNum}/${totalFromName} (GitHub Actions 1-based) -> index ${shardIndex} (internal 1-based)`);
274
- }
275
- }
276
-
277
- // If we couldn't extract from name but have job index, convert from 0-based array index to 1-based
278
- // GitHub Actions jobs are numbered starting from 1, so array index 0 = shard 1, index 1 = shard 2, etc.
279
- // We need to convert: array index 0 -> shard 1, array index 1 -> shard 2, etc.
280
- if (shardIndex === currentJobIndex && currentJobIndex >= 0) {
281
- shardIndex = currentJobIndex + 1; // Convert 0-based array index to 1-based shard index
282
- this.log(`🔍 [GitHub Actions Detection] Using array index ${currentJobIndex} -> shard ${shardIndex} (1-based)`);
283
- }
284
-
285
- const totalShards = jobGroup.length;
286
- this.log(`🔍 [GitHub Actions Detection] Detected via API:`);
287
- this.log(`🔍 Total jobs in group: ${totalShards}`);
288
- this.log(`🔍 Current job index: ${shardIndex}`);
289
- this.log(`🔍 Current job ID: ${currentJobId}`);
290
- this.log(`🔍 Current job name: ${currentJob?.name || 'unknown'}`);
291
- return {
292
- shardIndex,
293
- totalShards
294
- };
295
- } catch (error) {
296
- const apiUrl = `https://api.github.com/repos/${repo}/actions/runs/${runId}/jobs`;
297
- this.log(`🔍 [GitHub Actions Detection] Failed to detect via API: ${error.message}`);
298
- if (error.response) {
299
- const status = error.response.status;
300
- const data = error.response.data;
301
- this.log(`🔍 Status: ${status}, URL: ${apiUrl}`);
302
-
303
- if (status === 404) {
304
- this.log(`🔍 404 Error Details:`);
305
- this.log(`🔍 - Repository: ${repo}`);
306
- this.log(`🔍 - Run ID: ${runId}`);
307
- this.log(`🔍 - Possible causes:`);
308
- this.log(`🔍 1. Token lacks 'actions: read' permission`);
309
- this.log(`🔍 2. Workflow run doesn't exist or isn't accessible`);
310
- this.log(`🔍 3. Jobs not available yet (try accessing after workflow starts)`);
311
- this.log(`🔍 - Recommendation: Set MATRIX_SHARD and MATRIX_COUNT env vars as fallback`);
312
- } else if (status === 401 || status === 403) {
313
- this.log(`🔍 Authentication/Authorization Error:`);
314
- this.log(`🔍 - Token may be invalid or lack required permissions`);
315
- this.log(`🔍 - Required permission: 'actions: read' or 'repo' scope`);
316
- }
317
-
318
- if (data && data.message) {
319
- this.log(`🔍 Error message: ${data.message}`);
320
- }
321
- } else {
322
- this.log(`🔍 Network error or no response received`);
323
- }
324
- return null;
325
- }
326
- }
327
93
  async onEnd() {
328
94
  const { okToProceed, message } = this.preChecks();
329
95
  if (!okToProceed) {
@@ -333,209 +99,35 @@ class SlackReporter {
333
99
  // SHARDING SUPPORT - Stop slack messages for non-shard-1 shard(s)
334
100
  // Only shard 1 should post when aggregating results from multiple shards
335
101
 
336
- this.log('🔍 [Shard Detection] Starting shard detection logic...');
337
-
338
- // Check if we're actually running in GitHub Actions (not just env vars set locally)
339
- const isActuallyInGitHubActions = process.env.GITHUB_ACTIONS === 'true' &&
340
- process.env.GITHUB_RUN_ID !== undefined &&
341
- process.env.GITHUB_RUNNER_NAME !== undefined;
102
+ // Get shard info from environment variables
103
+ const currentShardIndex = process.env.SHARD_INDEX;
104
+ const totalShards = process.env.TOTAL_SHARDS ? parseInt(process.env.TOTAL_SHARDS, 10) : 1;
342
105
 
343
- // Log all relevant environment variables
344
- this.log(`🔍 [Shard Detection] Environment variables:`);
345
- this.log(`🔍 CI: ${process.env.CI !== undefined ? `"${process.env.CI}"` : 'undefined'}`);
346
- this.log(`🔍 GITHUB_ACTIONS: ${process.env.GITHUB_ACTIONS !== undefined ? `"${process.env.GITHUB_ACTIONS}"` : 'undefined'}`);
347
- this.log(`🔍 GITHUB_JOB: ${process.env.GITHUB_JOB !== undefined ? `"${process.env.GITHUB_JOB}"` : 'undefined'}`);
348
- this.log(`🔍 GITHUB_RUN_ID: ${process.env.GITHUB_RUN_ID !== undefined ? `"${process.env.GITHUB_RUN_ID}"` : 'undefined'}`);
349
- this.log(`🔍 GITHUB_RUNNER_NAME: ${process.env.GITHUB_RUNNER_NAME !== undefined ? `"${process.env.GITHUB_RUNNER_NAME}"` : 'undefined'}`);
350
- this.log(`🔍 MATRIX_SHARD: ${process.env.MATRIX_SHARD !== undefined ? `"${process.env.MATRIX_SHARD}"` : 'undefined'}`);
351
- this.log(`🔍 MATRIX_INDEX: ${process.env.MATRIX_INDEX !== undefined ? `"${process.env.MATRIX_INDEX}"` : 'undefined'}`);
352
- this.log(`🔍 MATRIX_COUNT: ${process.env.MATRIX_COUNT !== undefined ? `"${process.env.MATRIX_COUNT}"` : 'undefined'}`);
353
-
354
- // Check for Playwright shard environment variables (Playwright may set these)
355
- this.log(`🔍 PLAYWRIGHT_SHARD: ${process.env.PLAYWRIGHT_SHARD !== undefined ? `"${process.env.PLAYWRIGHT_SHARD}"` : 'undefined'}`);
356
- this.log(`🔍 SHARD_INDEX: ${process.env.SHARD_INDEX !== undefined ? `"${process.env.SHARD_INDEX}"` : 'undefined'}`);
357
- this.log(`🔍 TOTAL_SHARDS: ${process.env.TOTAL_SHARDS !== undefined ? `"${process.env.TOTAL_SHARDS}"` : 'undefined'}`);
358
-
359
- // Check Playwright's shard config (highest priority for local runs)
360
- let playwrightShardInfo = null;
361
- if (this.fullConfig) {
362
- // Debug: log config structure
363
- this.log(`🔍 [Shard Detection] Checking Playwright config for shard info...`);
364
- this.log(`🔍 fullConfig.shard: ${this.fullConfig.shard ? JSON.stringify(this.fullConfig.shard) : 'undefined'}`);
365
-
366
- if (this.fullConfig.shard) {
367
- // Playwright shard can be either { current, total } or undefined
368
- if (this.fullConfig.shard.current !== undefined && this.fullConfig.shard.total !== undefined) {
369
- playwrightShardInfo = {
370
- current: this.fullConfig.shard.current,
371
- total: this.fullConfig.shard.total
372
- };
373
- this.log(`🔍 [Shard Detection] Found Playwright shard config: ${playwrightShardInfo.current}/${playwrightShardInfo.total}`);
374
- } else {
375
- this.log(`🔍 [Shard Detection] Playwright shard config exists but missing current/total properties`);
376
- }
377
- } else {
378
- this.log(`🔍 [Shard Detection] No Playwright shard config found in fullConfig`);
379
- }
380
- } else {
381
- this.log(`🔍 [Shard Detection] fullConfig not available`);
382
- }
383
-
384
- // Also check if shard info is in process.argv (command line)
385
- // This is a fallback if config doesn't have it but --shard was passed
386
- if (!playwrightShardInfo && process.argv) {
387
- const shardArg = process.argv.find(arg => arg && arg.includes('--shard'));
388
- if (shardArg) {
389
- const shardMatch = shardArg.match(/--shard[=:]?(\d+)\/(\d+)/);
390
- if (shardMatch) {
391
- playwrightShardInfo = {
392
- current: parseInt(shardMatch[1], 10),
393
- total: parseInt(shardMatch[2], 10)
394
- };
395
- this.log(`🔍 [Shard Detection] Found shard in command line args: ${playwrightShardInfo.current}/${playwrightShardInfo.total} (1-based)`);
396
- }
397
- }
398
- }
399
-
400
- // Step 1: Try GitHub Actions API detection (only if actually in GitHub Actions)
401
- let githubApiShardInfo = null;
402
- if (isActuallyInGitHubActions) {
403
- this.log(`🔍 [Shard Detection] Actually running in GitHub Actions - attempting API detection...`);
404
- githubApiShardInfo = await this.detectGitHubActionsShardInfo();
405
- } else if (process.env.GITHUB_ACTIONS === 'true') {
406
- this.log(`🔍 [Shard Detection] GitHub Actions env vars set but not actually in runner - skipping API detection`);
407
- }
408
-
409
- // Step 2: Detect shard index (priority order: Playwright config > env vars > GitHub API)
410
- let currentShardIndex;
411
- if (playwrightShardInfo) {
412
- // Playwright uses 1-based indexing, keep it 1-based for shard 1 logic
413
- currentShardIndex = playwrightShardInfo.current.toString();
414
- this.log(`🔍 [Shard Detection] Using Playwright shard config (1-based): "${currentShardIndex}"`);
415
- } else if (process.env.SHARD_INDEX !== undefined) {
416
- currentShardIndex = process.env.SHARD_INDEX;
417
- this.log(`🔍 [Shard Detection] Using SHARD_INDEX env var: "${currentShardIndex}"`);
418
- } else if (process.env.MATRIX_SHARD !== undefined) {
419
- currentShardIndex = process.env.MATRIX_SHARD;
420
- this.log(`🔍 [Shard Detection] Using MATRIX_SHARD env var: "${currentShardIndex}"`);
421
- } else if (process.env.MATRIX_INDEX !== undefined) {
422
- currentShardIndex = process.env.MATRIX_INDEX;
423
- this.log(`🔍 [Shard Detection] Using MATRIX_INDEX env var: "${currentShardIndex}"`);
424
- } else if (githubApiShardInfo) {
425
- currentShardIndex = githubApiShardInfo.shardIndex.toString();
426
- this.log(`🔍 [Shard Detection] Using GitHub API detected shardIndex: "${currentShardIndex}"`);
427
- }
428
-
429
- this.log(`🔍 [Shard Detection] Initial currentShardIndex: ${currentShardIndex !== undefined ? `"${currentShardIndex}"` : 'undefined'}`);
430
-
431
- // Step 3: Get shard count from multiple sources (priority order)
432
- let totalShards;
433
- if (playwrightShardInfo) {
434
- totalShards = playwrightShardInfo.total;
435
- this.log(`🔍 [Shard Detection] Using Playwright shard config totalShards: ${totalShards}`);
436
- } else if (process.env.TOTAL_SHARDS !== undefined) {
437
- totalShards = parseInt(process.env.TOTAL_SHARDS, 10);
438
- this.log(`🔍 [Shard Detection] Using TOTAL_SHARDS env var: ${totalShards}`);
439
- } else if (process.env.MATRIX_COUNT !== undefined) {
440
- totalShards = parseInt(process.env.MATRIX_COUNT, 10);
441
- this.log(`🔍 [Shard Detection] Using MATRIX_COUNT env var: ${totalShards}`);
442
- } else if (githubApiShardInfo && githubApiShardInfo.totalShards > 1) {
443
- totalShards = githubApiShardInfo.totalShards;
444
- this.log(`🔍 [Shard Detection] Using GitHub API detected totalShards: ${totalShards}`);
445
- } else {
446
- // Fallback to ResultsParser
447
- const parserTotalShards = this.resultsParser.totalShardCount;
448
- totalShards = parserTotalShards;
449
- this.log(`🔍 [Shard Detection] Using ResultsParser totalShards: ${totalShards}`);
450
- }
451
-
452
- // Get shard info from ResultsParser for fallback
453
- const parserShardIndex = this.resultsParser.shardIndex;
454
- const parserTotalShards = this.resultsParser.totalShardCount;
455
-
456
- this.log(`🔍 [Shard Detection] ResultsParser values:`);
457
- this.log(`🔍 parserShardIndex: ${parserShardIndex !== undefined ? parserShardIndex : 'undefined'}`);
458
- this.log(`🔍 parserTotalShards: ${parserTotalShards !== undefined ? parserTotalShards : 'undefined'}`);
459
-
460
- this.log(`🔍 [Shard Detection] Calculated totalShards: ${totalShards}`);
461
-
462
- // If we still don't have shard index, try to infer from parser or GitHub API
463
- if (currentShardIndex === undefined) {
464
- if (githubApiShardInfo && githubApiShardInfo.shardIndex !== undefined) {
465
- currentShardIndex = githubApiShardInfo.shardIndex.toString();
466
- this.log(`🔍 [Shard Detection] Using GitHub API shardIndex as fallback: "${currentShardIndex}"`);
467
- } else if (parserShardIndex !== undefined) {
468
- currentShardIndex = parserShardIndex.toString();
469
- this.log(`🔍 [Shard Detection] Using parserShardIndex as fallback: "${currentShardIndex}"`);
470
- }
471
- }
472
-
473
- // CRITICAL: In CI (especially GitHub Actions), if we can't detect shard info, block posting
106
+ // CRITICAL: In CI, if we can't detect shard info, block posting
474
107
  // This prevents duplicate messages when running with multiple shards
475
- // BUT: Don't block if we're running locally (even if CI env vars are set)
476
- // Note: GitHub Actions uses 1-based indexing (shard 1, 2, 3, 4), but we use 0-based internally
477
- if (isActuallyInGitHubActions && currentShardIndex === undefined) {
478
- if (process.env.GITHUB_ACTIONS === 'true') {
479
- this.log(`❌ [Shard Detection] BLOCKING: GitHub Actions detected but shard index cannot be determined.`);
480
- this.log(`❌ Attempted: GitHub API detection ${githubApiShardInfo ? 'succeeded' : 'failed'}, env vars not set`);
481
- this.log(`❌ This prevents duplicate messages when running with multiple shards.`);
482
- } else {
483
- this.log(`❌ [Shard Detection] BLOCKING: CI environment detected but shard info not set.`);
484
- this.log(`❌ MATRIX_COUNT: undefined, MATRIX_SHARD: undefined`);
485
- this.log(`❌ This prevents duplicate messages when running with multiple shards.`);
486
- }
108
+ if (process.env.CI && currentShardIndex === undefined && totalShards > 1) {
109
+ this.log(`❌ [Shard Detection] BLOCKING: CI environment detected but SHARD_INDEX not set.`);
110
+ this.log(`❌ This prevents duplicate messages when running with multiple shards.`);
487
111
  this.log(`❌ Skipping Slack report to prevent duplicate messages from all shards.`);
488
- this.log(`💡 Fix: Add these env vars to your workflow:`);
112
+ this.log(`💡 Fix: Add SHARD_INDEX env var to your workflow:`);
489
113
  this.log(`💡 env:`);
490
- this.log(`💡 MATRIX_SHARD: $\{\{ strategy.job-index \}\} # GitHub Actions uses 1-based, convert to 0-based (only shard 1/internal index 1 posts)`);
491
- this.log(`💡 MATRIX_COUNT: $\{\{ strategy.job-total \}\}`);
492
- this.log(`💡 OR ensure SAFETYWINGTEST_GITHUB_TOKEN has workflow permissions for API detection`);
114
+ this.log(`💡 SHARD_INDEX: $\{\{ strategy.job-index \}\} # Only shard 1 should post`);
115
+ this.log(`💡 TOTAL_SHARDS: $\{\{ strategy.job-total \}\}`);
493
116
  return;
494
117
  }
495
118
 
496
119
  // Convert to number for comparison (default to 1 if undefined, since shard 1 is the aggregator)
497
120
  const shardIndexNum = currentShardIndex !== undefined ? parseInt(currentShardIndex, 10) : 1;
498
121
 
499
- this.log(`🔍 [Shard Detection] Final values:`);
500
- this.log(`🔍 currentShardIndex (string): ${currentShardIndex !== undefined ? `"${currentShardIndex}"` : 'undefined'}`);
501
- this.log(`🔍 shardIndexNum (number): ${shardIndexNum}`);
502
- this.log(`🔍 totalShards: ${totalShards}`);
503
-
504
122
  // CRITICAL: Update ResultsParser with detected shard values immediately after detection
505
123
  // This ensures ResultsParser always has correct values, even if we block posting
506
124
  this.resultsParser.updateShardInfo(shardIndexNum, totalShards);
507
125
 
508
- // For local runs with CI env vars set but not actually in CI, allow posting if single shard
509
- // or if shard index is 1
510
- if (!isActuallyInGitHubActions && process.env.CI && totalShards > 1 && currentShardIndex === undefined) {
511
- this.log(`âš ī¸ [Shard Detection] Local run with CI env vars but shard info unclear - allowing post (may cause duplicates)`);
512
- }
513
-
514
- // CRITICAL: For GitHub Actions with multiple shards, only shard 1 should post
515
- // If we're in CI with multiple shards and don't know which shard we are, block all posts
516
- // This prevents duplicate messages when MATRIX_SHARD/MATRIX_INDEX is not set
517
- // Note: GitHub Actions uses 1-based indexing (shard 1, 2, 3, 4)
518
- // We use 0-based internally, so GitHub Actions shard 1 = internal index 1
519
- if (isActuallyInGitHubActions && totalShards > 1 && currentShardIndex === undefined) {
520
- this.log(`❌ [Shard Detection] BLOCKING: GitHub Actions detected with ${totalShards} shards but shard index not set (MATRIX_SHARD/MATRIX_INDEX missing).`);
521
- this.log(`❌ Skipping Slack report to prevent duplicate messages from all shards.`);
522
- this.log(`💡 Fix: Add these env vars to your workflow:`);
523
- this.log(`💡 env:`);
524
- this.log(`💡 MATRIX_SHARD: $\{\{ strategy.job-index \}\} # GitHub Actions uses 1-based, convert to 0-based (only shard 1/internal index 1 posts)`);
525
- this.log(`💡 MATRIX_COUNT: $\{\{ strategy.job-total \}\}`);
526
- this.log(`💡 OR ensure SAFETYWINGTEST_GITHUB_TOKEN has workflow permissions for API detection`);
527
- return;
528
- }
529
-
530
126
  // Only allow shard 1 to post when there are multiple shards
531
127
  // This ensures only one shard posts the aggregated report
532
- // Note: GitHub Actions uses 1-based indexing (shard 1, 2, 3, 4)
533
- // We use 0-based internally, so shard 1 in GitHub Actions = index 1 internally
534
128
  if (totalShards > 1 && shardIndexNum !== 1) {
535
- // Convert back to 1-based for display if from GitHub Actions
536
- const displayShard = isActuallyInGitHubActions ? shardIndexNum + 1 : shardIndexNum;
537
- this.log(`❌ [Shard Detection] BLOCKING: Non-shard-1 detected (shard ${displayShard} of ${totalShards}${isActuallyInGitHubActions ? ' [GitHub Actions 1-based]' : ''})`);
538
- this.log(`❌ Stopping reporter for shard ${displayShard} of ${totalShards}`);
129
+ this.log(`❌ [Shard Detection] BLOCKING: Non-shard-1 detected (shard ${shardIndexNum} of ${totalShards})`);
130
+ this.log(`❌ Stopping reporter for shard ${shardIndexNum} of ${totalShards}`);
539
131
  this.log(`â„šī¸ Only shard 1 will post the aggregated report.`);
540
132
  return;
541
133
  }
@@ -544,9 +136,7 @@ class SlackReporter {
544
136
  if (totalShards === 1) {
545
137
  this.log(`✅ [Shard Detection] ALLOWING: Single shard detected. Posting Slack report.`);
546
138
  } else {
547
- // Convert back to 1-based for display if from GitHub Actions
548
- const displayShard = isActuallyInGitHubActions ? shardIndexNum + 1 : shardIndexNum;
549
- this.log(`✅ [Shard Detection] ALLOWING: Multiple shards detected (${totalShards}). Shard ${displayShard} (index ${shardIndexNum}${isActuallyInGitHubActions ? ', GitHub Actions 1-based' : ''}) will post aggregated report.`);
139
+ this.log(`✅ [Shard Detection] ALLOWING: Multiple shards detected (${totalShards}). Shard ${shardIndexNum} will post aggregated report.`);
550
140
  }
551
141
 
552
142
  const resultSummary = await this.resultsParser.getParsedResults();
package/package.json CHANGED
@@ -32,7 +32,7 @@
32
32
  "lint-fix": "npx eslint . --ext .ts --fix"
33
33
  },
34
34
  "name": "playwright-slack-report-burak",
35
- "version": "3.0.20",
35
+ "version": "3.0.22",
36
36
  "main": "index.js",
37
37
  "types": "dist/index.d.ts",
38
38
  "author": "Burak B. <burak.boluk@hotmail.com>",