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.
- package/dist/src/ResultsParser.js +15 -65
- package/dist/src/SlackReporter.js +13 -423
- package/package.json +1 -1
|
@@ -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
|
-
:
|
|
38
|
+
: 1;
|
|
41
39
|
shardIndex = process.env.SHARD_INDEX !== undefined
|
|
42
40
|
? parseInt(process.env.SHARD_INDEX, 10)
|
|
43
|
-
:
|
|
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
|
|
443
|
-
// 2. blob-report-node-{i} (direct blob reports
|
|
444
|
-
// 3. test-results-{i} (alternative naming
|
|
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}`,
|
|
448
|
-
`blob-report-node-${i}`,
|
|
449
|
-
`
|
|
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(`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
|
112
|
+
this.log(`đĄ Fix: Add SHARD_INDEX env var to your workflow:`);
|
|
489
113
|
this.log(`đĄ env:`);
|
|
490
|
-
this.log(`đĄ
|
|
491
|
-
this.log(`đĄ
|
|
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
|
-
|
|
536
|
-
|
|
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
|
-
|
|
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