haven-cypress-integration 1.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.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # Haven-Cypress Integration
2
+
3
+ Seamless integration between Cypress test frameworks and HAVEN test case management system.
4
+
5
+ ## Quick Start
6
+
7
+ ### 1. Install
8
+ ```bash
9
+ npm install haven-cypress-integration
10
+ ```
11
+
12
+ ### 2. Add Tags to Tests
13
+ Add `@TC-AUTO-XXXX` tags to your test descriptions:
14
+
15
+ ```javascript
16
+ it("Login test @TC-AUTO-123, @p2", () => {
17
+ cy.visit('/login');
18
+ cy.get('[data-cy=username]').type('user');
19
+ cy.get('[data-cy=password]').type('pass');
20
+ cy.get('[data-cy=submit]').click();
21
+ });
22
+
23
+ it("User registration @TC-AUTO-124, @p1", () => {
24
+ cy.visit('/register');
25
+ // your test code
26
+ });
27
+ ```
28
+
29
+ ### 3. Build Docker Image
30
+ ```bash
31
+ npx haven-cypress build
32
+ ```
33
+
34
+ ### 4. Deploy to Haven
35
+ Your Docker image is now ready to be deployed and run by HAVEN!
36
+
37
+ ## Commands
38
+
39
+ ### Build Image
40
+ ```bash
41
+ # Basic build
42
+ npx haven-cypress build
43
+
44
+ # Custom tag
45
+ npx haven-cypress build --tag=my-tests:v1.0
46
+
47
+ # Build and push to registry
48
+ npx haven-cypress build --tag=my-tests:v1.0 --push
49
+ ```
50
+
51
+ ### Run Tests
52
+ ```bash
53
+ # Run all tests
54
+ npx haven-cypress run
55
+
56
+ # Run specific test cases
57
+ npx haven-cypress run --automationIds=TC-AUTO-123,TC-AUTO-124
58
+ ```
59
+
60
+ ## What's Included
61
+
62
+ - ✅ **Tag-based test filtering** (`@TC-AUTO-XXXX`)
63
+ - ✅ **Mochawesome reporting** with screenshots
64
+ - ✅ **S3 artifact upload** (reports, logs, screenshots)
65
+ - ✅ **HAVEN API integration** (result synchronization)
66
+ - ✅ **Docker containerization** ready for HAVEN deployment
67
+
68
+ ## How It Works
69
+
70
+ 1. **Haven runs your container** with mounted configuration and environment variables
71
+ 2. **Your tests run** with tag filtering based on automation IDs
72
+ 3. **Results are generated** using Mochawesome reporting
73
+ 4. **Artifacts are uploaded** to S3 for review
74
+ 5. **Results sync back** to HAVEN via API integration
75
+
76
+ ## Requirements
77
+
78
+ - Node.js 14+
79
+ - Docker
80
+ - Existing Cypress project
81
+ - HAVEN access credentials (provided by Haven when container runs)
82
+
83
+ ## Support
84
+
85
+ For issues or questions, contact your HAVEN administrator.
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ const HavenCypressIntegration = require('../index.js');
4
+
5
+ const args = process.argv.slice(2);
6
+ const command = args[0];
7
+
8
+ if (!command) {
9
+ console.log(`
10
+ Haven-Cypress Integration
11
+
12
+ Usage: npx haven-cypress <command> [options]
13
+
14
+ Commands:
15
+ build [options] Build Docker image with Haven integration
16
+ run [options] Run tests with Haven integration
17
+
18
+ Options:
19
+ --tag=name:version Docker image tag (default: haven-cypress-tests:latest)
20
+ --push Push image to registry after building
21
+ --automationIds=ID1,ID2 Run specific test cases
22
+
23
+ Examples:
24
+ npx haven-cypress build
25
+ npx haven-cypress build --tag=my-tests:v1.0 --push
26
+ npx haven-cypress run --automationIds=TC-AUTO-123,TC-AUTO-124
27
+ `);
28
+ process.exit(1);
29
+ }
30
+
31
+ const integration = new HavenCypressIntegration();
32
+
33
+ // Parse options
34
+ const options = {};
35
+ args.slice(1).forEach(arg => {
36
+ if (arg.startsWith('--')) {
37
+ const [key, value] = arg.substring(2).split('=');
38
+ options[key] = value || true;
39
+ }
40
+ });
41
+
42
+ try {
43
+ switch (command) {
44
+ case 'build':
45
+ const tag = options.tag || 'haven-cypress-tests:latest';
46
+ integration.buildImage(tag, options);
47
+ break;
48
+
49
+ case 'run':
50
+ const automationIds = options.automationIds || '';
51
+ integration.runTests(automationIds);
52
+ break;
53
+
54
+ default:
55
+ console.error(`❌ Unknown command: ${command}`);
56
+ process.exit(1);
57
+ }
58
+ } catch (error) {
59
+ console.error(`❌ Error: ${error.message}`);
60
+ process.exit(1);
61
+ }
package/index.js ADDED
@@ -0,0 +1,146 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+
5
+ class HavenCypressIntegration {
6
+ constructor() {
7
+ this.templatesDir = path.join(__dirname, 'templates');
8
+ }
9
+
10
+ /**
11
+ * Build Docker image with Haven integration
12
+ * @param {string} tag - Docker image tag
13
+ * @param {Object} options - Build options
14
+ */
15
+ buildImage(tag = 'haven-cypress-tests:latest', options = {}) {
16
+ console.log('🐳 Building Haven-integrated Docker image...');
17
+
18
+ // Create temporary build directory
19
+ const buildDir = path.join(process.cwd(), '.haven-build');
20
+ if (fs.existsSync(buildDir)) {
21
+ fs.rmSync(buildDir, { recursive: true });
22
+ }
23
+ fs.mkdirSync(buildDir, { recursive: true });
24
+
25
+ try {
26
+ // Copy user's project files
27
+ console.log('📁 Copying project files...');
28
+ this.copyProjectFiles(buildDir);
29
+
30
+ // Copy Haven integration files
31
+ console.log('🔧 Adding Haven integration files...');
32
+ this.copyHavenFiles(buildDir);
33
+
34
+ // Build Docker image
35
+ console.log('🏗️ Building Docker image...');
36
+ execSync(`docker build -t ${tag} .`, {
37
+ cwd: buildDir,
38
+ stdio: 'inherit'
39
+ });
40
+
41
+ console.log(`✅ Docker image built successfully: ${tag}`);
42
+
43
+ // Push if requested
44
+ if (options.push) {
45
+ console.log('📤 Pushing image to registry...');
46
+ execSync(`docker push ${tag}`, { stdio: 'inherit' });
47
+ console.log('✅ Image pushed successfully');
48
+ }
49
+
50
+ } finally {
51
+ // Cleanup build directory
52
+ if (fs.existsSync(buildDir)) {
53
+ fs.rmSync(buildDir, { recursive: true });
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Copy user's project files to build directory
60
+ */
61
+ copyProjectFiles(buildDir) {
62
+ const filesToCopy = [
63
+ 'package.json',
64
+ 'package-lock.json',
65
+ 'cypress.config.js',
66
+ 'cypress'
67
+ ];
68
+
69
+ filesToCopy.forEach(file => {
70
+ const src = path.join(process.cwd(), file);
71
+ const dest = path.join(buildDir, file);
72
+
73
+ if (fs.existsSync(src)) {
74
+ if (fs.statSync(src).isDirectory()) {
75
+ this.copyDir(src, dest);
76
+ } else {
77
+ fs.copyFileSync(src, dest);
78
+ }
79
+ console.log(` ✅ Copied ${file}`);
80
+ } else {
81
+ console.log(` ⚠️ Skipped ${file} (not found)`);
82
+ }
83
+ });
84
+ }
85
+
86
+ /**
87
+ * Copy Haven integration files to build directory
88
+ */
89
+ copyHavenFiles(buildDir) {
90
+ const havenFiles = [
91
+ 'Dockerfile',
92
+ 'run-filtered.sh',
93
+ 'syncCypressResults.js'
94
+ ];
95
+
96
+ havenFiles.forEach(file => {
97
+ const src = path.join(this.templatesDir, file);
98
+ const dest = path.join(buildDir, file);
99
+
100
+ if (fs.existsSync(src)) {
101
+ fs.copyFileSync(src, dest);
102
+ console.log(` ✅ Added ${file}`);
103
+ }
104
+ });
105
+
106
+ // Make run-filtered.sh executable
107
+ execSync('chmod +x run-filtered.sh', { cwd: buildDir });
108
+ }
109
+
110
+ /**
111
+ * Copy directory recursively
112
+ */
113
+ copyDir(src, dest) {
114
+ if (!fs.existsSync(dest)) {
115
+ fs.mkdirSync(dest, { recursive: true });
116
+ }
117
+
118
+ const entries = fs.readdirSync(src, { withFileTypes: true });
119
+
120
+ for (const entry of entries) {
121
+ const srcPath = path.join(src, entry.name);
122
+ const destPath = path.join(dest, entry.name);
123
+
124
+ if (entry.isDirectory()) {
125
+ this.copyDir(srcPath, destPath);
126
+ } else {
127
+ fs.copyFileSync(srcPath, destPath);
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Run tests with Haven integration
134
+ * This is called by Haven when the container runs
135
+ */
136
+ runTests(automationIds = '') {
137
+ console.log('🧪 Running Haven-integrated Cypress tests...');
138
+ console.log(`🔍 Automation IDs: ${automationIds || 'All tests'}`);
139
+
140
+ // This will be handled by run-filtered.sh when container runs
141
+ // The library just provides the interface
142
+ return { success: true, message: 'Tests will run via run-filtered.sh' };
143
+ }
144
+ }
145
+
146
+ module.exports = HavenCypressIntegration;
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "haven-cypress-integration",
3
+ "version": "1.0.0",
4
+ "description": "Seamless Cypress integration with HAVEN test case management",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "haven-cypress": "./bin/haven-cypress.js"
8
+ },
9
+ "keywords": [
10
+ "cypress",
11
+ "haven",
12
+ "testing",
13
+ "integration",
14
+ "docker"
15
+ ],
16
+ "author": "BillExplainer",
17
+ "license": "ISC",
18
+ "files": [
19
+ "index.js",
20
+ "bin/",
21
+ "templates/"
22
+ ],
23
+ "engines": {
24
+ "node": ">=14.0.0"
25
+ }
26
+ }
@@ -0,0 +1,29 @@
1
+ FROM cypress/included:14.3.1
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy project files
6
+ COPY . .
7
+
8
+ # ✅ Install zip and AWS CLI v2 via official method
9
+ RUN apt-get update && \
10
+ apt-get install -y \
11
+ zip \
12
+ unzip \
13
+ curl && \
14
+ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
15
+ unzip awscliv2.zip && \
16
+ ./aws/install && \
17
+ rm -rf aws awscliv2.zip
18
+
19
+ # ✅ Install Node deps
20
+ RUN npm ci
21
+ RUN npm install --no-save aws-sdk
22
+
23
+ # ✅ Ensure script is executable
24
+ COPY run-filtered.sh /app/run-filtered.sh
25
+ RUN chmod +x /app/run-filtered.sh
26
+
27
+ # ✅ Entrypoint
28
+ ENTRYPOINT ["/app/run-filtered.sh"]
29
+ CMD []
@@ -0,0 +1,129 @@
1
+ #!/bin/bash
2
+
3
+ echo "🔥 RUN SCRIPT:"
4
+ echo "✅ ENTERED run-filtered.sh"
5
+ echo "⚙️ Raw args: $@"
6
+
7
+ # Extract automation IDs from CLI args
8
+ AUTOMATION_IDS=""
9
+ for arg in "$@"; do
10
+ case $arg in
11
+ --automationIds=*)
12
+ AUTOMATION_IDS="${arg#*=}"
13
+ shift
14
+ ;;
15
+ esac
16
+ done
17
+
18
+ echo "🔍 Extracted automation IDs: ${AUTOMATION_IDS}"
19
+ echo "📂 Current working directory: $(pwd)"
20
+
21
+
22
+ if [ -z "$AUTOMATION_IDS" ]; then
23
+ echo "🔁 No automation IDs provided. Running all tests..."
24
+ CYPRESS_GREP=""
25
+ else
26
+ # Strip quotes from arg, replace commas with spaces
27
+ RAW_IDS=$(echo "$AUTOMATION_IDS" | sed "s/^['\"]//;s/['\"]$//")
28
+ CLEANED_IDS="${RAW_IDS//,/ }"
29
+
30
+ echo "🚀 Running Cypress with filtered tags: ${CLEANED_IDS}"
31
+ CYPRESS_GREP="--env grepTags='${CLEANED_IDS}'"
32
+ fi
33
+
34
+ # Set environment variables
35
+ TIMESTAMP=$(date +%m%d%Y_%H%M%S)
36
+ PRODUCT_NAME=${PRODUCT_NAME:-unknown_product}
37
+ PLAN_ID=${PLAN_ID:-unknown_plan}
38
+ RUN_ID=${RUN_ID:-unknown_run}
39
+ BUCKET_NAME=${BUCKET_NAME:-your-default-bucket}
40
+
41
+ # Ensure results directory exists
42
+ mkdir -p results/mochawesome
43
+
44
+ echo "🧹 Cleaning mochawesome reports"
45
+ rm -rf results/mochawesome/*
46
+
47
+ # Run Cypress
48
+ echo "🚀 Running Cypress with filtered tags: ${AUTOMATION_IDS}"
49
+ echo "💡 Final grep arg: $CYPRESS_GREP"
50
+ eval "npx cypress run \
51
+ --headless \
52
+ --browser chrome \
53
+ $CYPRESS_GREP \
54
+ > results/logs.txt 2>&1"
55
+
56
+ CYPRESS_EXIT_CODE=$?
57
+
58
+ # Smart HTML report generation logic
59
+ MOCHAWESOME_DIR="results/mochawesome"
60
+ MERGED_JSON="results/results.json"
61
+ REPORT_HTML="${MOCHAWESOME_DIR}/report.html"
62
+
63
+ echo "🛠️ Attempting to merge mochawesome JSON files..."
64
+ npx mochawesome-merge ${MOCHAWESOME_DIR}/*.json > "${MERGED_JSON}"
65
+
66
+ if [ -s "${MERGED_JSON}" ]; then
67
+ echo "✅ Merge successful. Generating report from merged JSON."
68
+ npx marge "${MERGED_JSON}" --reportDir "${MOCHAWESOME_DIR}" --reportFilename report.html
69
+ else
70
+ echo "⚠️ Merge failed or empty. Falling back to first mochawesome JSON file..."
71
+ FIRST_JSON=$(ls ${MOCHAWESOME_DIR}/*.json 2>/dev/null | head -n 1)
72
+ if [ -f "${FIRST_JSON}" ]; then
73
+ npx marge "${FIRST_JSON}" --reportDir "${MOCHAWESOME_DIR}" --reportFilename report.html
74
+ else
75
+ echo "❌ No valid mochawesome JSON file found for fallback."
76
+ fi
77
+ fi
78
+
79
+ # Copy screenshots (if any)
80
+ SCREENSHOTS_DIR="cypress/screenshots"
81
+ DEST_SCREENSHOTS_DIR="${MOCHAWESOME_DIR}/screenshots"
82
+ if [ -d "$SCREENSHOTS_DIR" ]; then
83
+ echo "🖼️ Copying screenshots..."
84
+ mkdir -p "$DEST_SCREENSHOTS_DIR"
85
+ cp -r "$SCREENSHOTS_DIR"/* "$DEST_SCREENSHOTS_DIR" || echo "⚠️ No screenshots to copy"
86
+ else
87
+ echo "ℹ️ No screenshots directory found."
88
+ fi
89
+
90
+ # Zip the mochawesome report
91
+ ZIP_NAME="${PRODUCT_NAME}_${TIMESTAMP}.zip"
92
+ ZIP_PATH="/tmp/${ZIP_NAME}"
93
+
94
+ if [ -d "${MOCHAWESOME_DIR}" ]; then
95
+ echo "📦 Zipping mochawesome report and screenshots..."
96
+ zip -r "${ZIP_PATH}" "${MOCHAWESOME_DIR}" > /dev/null
97
+ else
98
+ echo "❌ Mochawesome report folder not found: ${MOCHAWESOME_DIR}"
99
+ fi
100
+
101
+ # Upload zipped report to S3
102
+ if [ -f "${ZIP_PATH}" ]; then
103
+ S3_BASE_KEY="artifact/${PLAN_ID}/automation/${RUN_ID}"
104
+ S3_ZIP_KEY="${S3_BASE_KEY}/${ZIP_NAME}"
105
+ echo "☁️ Uploading ZIP to s3://${BUCKET_NAME}/${S3_ZIP_KEY}"
106
+ aws s3 cp "${ZIP_PATH}" "s3://${BUCKET_NAME}/${S3_ZIP_KEY}" || echo "❌ S3 ZIP upload failed"
107
+ else
108
+ echo "❌ No zip file to upload"
109
+ fi
110
+
111
+ # Upload HTML report file to S3
112
+ if [ -f "${REPORT_HTML}" ]; then
113
+ HTML_NAME=$(basename "$REPORT_HTML")
114
+ S3_HTML_KEY="${S3_BASE_KEY}/${HTML_NAME}"
115
+ echo "🌐 Uploading HTML to s3://${BUCKET_NAME}/${S3_HTML_KEY}"
116
+ aws s3 cp "${REPORT_HTML}" "s3://${BUCKET_NAME}/${S3_HTML_KEY}" || echo "❌ S3 HTML upload failed"
117
+ else
118
+ echo "❌ No HTML report found to upload"
119
+ fi
120
+
121
+ # Run result sync
122
+ echo "🔄 Running syncCypressResults.js"
123
+ PLAN_ID="${PLAN_ID}" RUN_ID="${RUN_ID}" node syncCypressResults.js >> results/logs.txt 2>&1 || echo "⚠️ syncCypressResults.js failed"
124
+
125
+ # Final output
126
+ echo "📤 Dumping results/logs.txt for runner capture:"
127
+ cat results/logs.txt || echo "❌ Failed to read results/logs.txt"
128
+
129
+ exit $CYPRESS_EXIT_CODE
@@ -0,0 +1,218 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const axios = require("axios");
4
+ const glob = require("glob");
5
+ const AWS = require("aws-sdk");
6
+
7
+ // AWS setup
8
+ const s3 = new AWS.S3({
9
+ region: process.env.AWS_REGION || "us-east-1",
10
+ });
11
+ const bucketName = process.env.S3_BUCKET || "your-bucket-name";
12
+
13
+ // Paths
14
+ const automationCasesPath = "/shared/automation-cases.json";
15
+ const postBaseUrlPath = "/shared/result-post-url.txt";
16
+ const baseUrl = fs.readFileSync(postBaseUrlPath, "utf-8").trim();
17
+
18
+ (async () => {
19
+ let automationIds = null;
20
+ const formatted = [];
21
+ const foundIds = new Set();
22
+
23
+ try {
24
+ const expectedCases = JSON.parse(
25
+ fs.readFileSync(automationCasesPath, "utf-8")
26
+ );
27
+ const ids = expectedCases
28
+ .map((c) => (c.automation_id || "").trim())
29
+ .filter(Boolean);
30
+ if (ids.length > 0) automationIds = new Set(ids);
31
+ } catch (err) {
32
+ console.warn(
33
+ "⚠️ No valid automation-cases.json found. Using @regression fallback."
34
+ );
35
+ }
36
+
37
+ const mochawesomeDir = path.resolve("results/mochawesome");
38
+ console.log("Looking for mochawesome JSONs in:", `${mochawesomeDir}/*.json`);
39
+ const jsonFiles = glob.sync(`${mochawesomeDir}/*.json`);
40
+
41
+ if (jsonFiles.length === 0) {
42
+ console.error("❌ No mochawesome report files found.");
43
+ process.exit(1);
44
+ }
45
+
46
+ console.log(`📄 Found ${jsonFiles.length} mochawesome report(s).`);
47
+ const combinedResults = { results: [] };
48
+
49
+ try {
50
+ for (const file of jsonFiles) {
51
+ const data = JSON.parse(fs.readFileSync(file, "utf-8"));
52
+ if (data?.results) {
53
+ combinedResults.results.push(...data.results);
54
+ }
55
+ }
56
+ } catch (err) {
57
+ console.error("❌ Failed to parse mochawesome JSON:", err.message);
58
+ process.exit(1);
59
+ }
60
+
61
+ function walkSuites(suite) {
62
+ collectTests(suite.tests);
63
+ (suite.suites || []).forEach(walkSuites);
64
+ }
65
+
66
+ function collectTests(tests) {
67
+ (tests || []).forEach((test) => {
68
+ const titleCombined =
69
+ typeof test.fullTitle === "string"
70
+ ? test.fullTitle
71
+ : Array.isArray(test.title)
72
+ ? test.title.join(" ")
73
+ : test.title;
74
+
75
+ const allTags = [...(titleCombined.match(/@[\w-]+/g) || [])];
76
+
77
+ const status =
78
+ test.state === "passed" || test.pass === true
79
+ ? "pass"
80
+ : test.pending === true
81
+ ? "skipped"
82
+ : "fail";
83
+
84
+ if (!automationIds) {
85
+ allTags
86
+ .filter((tag) => /^TC-AUTO-\w+$/i.test(tag))
87
+ .forEach((tag) => {
88
+ formatted.push({ automation_id: tag, status });
89
+ foundIds.add(tag);
90
+ });
91
+ } else {
92
+ allTags
93
+ .filter((tag) => automationIds.has(tag))
94
+ .forEach((tag) => {
95
+ formatted.push({ automation_id: tag, status });
96
+ foundIds.add(tag);
97
+ });
98
+ }
99
+ });
100
+ }
101
+
102
+ (combinedResults.results || []).forEach(walkSuites);
103
+
104
+ const notFound = automationIds
105
+ ? [...automationIds].filter((id) => !foundIds.has(id))
106
+ : [];
107
+
108
+ const planId = process.env.PLAN_ID || "unknown";
109
+ const runId = process.env.RUN_ID || "unknown";
110
+
111
+ if (!planId || !runId || isNaN(Number(planId)) || isNaN(Number(runId))) {
112
+ console.error("❌ Invalid or missing PLAN_ID / RUN_ID");
113
+ process.exit(1);
114
+ }
115
+
116
+ await postResults(formatted, notFound, planId, runId);
117
+ await postSummary(formatted, notFound, planId, runId);
118
+ })();
119
+
120
+ async function postResults(formattedResults, notFoundList, planId, runId) {
121
+ const postUrl = `${baseUrl}/api/test-results`;
122
+
123
+ const payload = {
124
+ planId,
125
+ runId,
126
+ results: formattedResults,
127
+ not_found: notFoundList,
128
+ };
129
+
130
+ console.log(
131
+ "📤 Posting result from sync report:",
132
+ JSON.stringify(payload, null, 2)
133
+ );
134
+
135
+ try {
136
+ const response = await axios.post(postUrl, payload, {
137
+ headers: { "Content-Type": "application/json" },
138
+ });
139
+ console.log("✅ Test case results posted:", response.status);
140
+ } catch (err) {
141
+ console.error(
142
+ "❌ Failed to post test case results:",
143
+ err.response?.status,
144
+ err.response?.data || err.message
145
+ );
146
+ process.exit(1);
147
+ }
148
+ }
149
+
150
+ async function postSummary(results, notFound, planId, runId) {
151
+ const url = `${baseUrl}/api/test-run-summary`;
152
+
153
+ const logs =
154
+ (await uploadLogToS3(planId, runId)) || "Log upload failed or missing";
155
+
156
+ const summaryPayload = {
157
+ runId,
158
+ planId,
159
+ status: computeOverallStatus(results, notFound),
160
+ logs,
161
+ result: {
162
+ total: results.length,
163
+ passed: results.filter((r) => r.status === "pass").length,
164
+ failed: results.filter((r) => r.status === "fail").length,
165
+ skipped: results.filter((r) => r.status === "skipped").length,
166
+ not_found: notFound.length,
167
+ },
168
+ };
169
+
170
+ console.log("📤 Posting test run summary to:", url);
171
+ console.log("📦 Summary payload:", JSON.stringify(summaryPayload, null, 2));
172
+
173
+ try {
174
+ const resp = await axios.post(url, summaryPayload, {
175
+ headers: { "Content-Type": "application/json" },
176
+ });
177
+ console.log("✅ Test run summary posted:", resp.data.message);
178
+ } catch (err) {
179
+ console.error(
180
+ "❌ Failed to post summary:",
181
+ err.response?.data || err.message
182
+ );
183
+ }
184
+ }
185
+
186
+ async function uploadLogToS3(planId, runId) {
187
+ const logsPath = "results/logs.txt";
188
+
189
+ if (!fs.existsSync(logsPath)) {
190
+ console.warn("⚠️ No logs.txt file found to upload.");
191
+ return null;
192
+ }
193
+
194
+ const key = `artifact/${planId}/automation/${runId}/run_log`;
195
+ const fileStream = fs.createReadStream(logsPath);
196
+
197
+ try {
198
+ await s3
199
+ .upload({
200
+ Bucket: bucketName,
201
+ Key: key,
202
+ Body: fileStream,
203
+ ContentType: "text/plain",
204
+ })
205
+ .promise();
206
+
207
+ return `s3://${bucketName}/${key}`;
208
+ } catch (err) {
209
+ console.error("❌ Failed to upload log to S3:", err.message);
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function computeOverallStatus(results, notFound) {
215
+ if (!results.length && !notFound.length) return "skipped";
216
+ const hasFail = results.some((r) => r.status === "fail");
217
+ return hasFail ? "failed" : "passed";
218
+ }