Skip to content

Commit 215ecfb

Browse files
committed
ci: improve Hugo process management to address EPIPE errors in Github Actions. - Proactive monitoring: Detecting when Hugo dies
during execution - Resource management: Reducing memory pressure in CI that causes process termination - Signal handling: Properly cleaning up processes on unexpected termination - Timeout adjustments: Giving more time for operations in CI environments 3. The Test Setup Validation Failure: This was happening because the before() hook was failing when it couldn't communicate with a dead Hugo process. Your health monitoring will catch this earlier and provide better error messages.
1 parent 8a26400 commit 215ecfb

File tree

2 files changed

+127
-42
lines changed

2 files changed

+127
-42
lines changed

.github/actions/validate-links/action.yml

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -37,30 +37,42 @@ runs:
3737
${{ inputs.cache-key }}-
3838
3939
- name: Run link validation
40-
id: validate
40+
shell: bash
4141
run: |
42-
echo "Testing files: ${{ inputs.files }}"
43-
if [[ -n "${{ inputs.product-name }}" ]]; then
44-
echo "Product: ${{ inputs.product-name }}"
45-
fi
42+
# Set CI-specific environment variables
43+
export CI=true
44+
export GITHUB_ACTIONS=true
45+
export NODE_OPTIONS="--max-old-space-size=4096"
4646
47-
if [[ "${{ inputs.cache-enabled }}" == "true" ]]; then
48-
echo "📦 Cache enabled for this validation run"
49-
fi
47+
# Add timeout to prevent hanging
48+
timeout 30m node cypress/support/run-e2e-specs.js ${{ inputs.files }} \
49+
--spec cypress/e2e/content/article-links.cy.js || {
50+
exit_code=$?
51+
echo "::error::Link validation failed with exit code $exit_code"
52+
53+
# Check for specific error patterns
54+
if [ -f hugo.log ]; then
55+
echo "::group::Hugo Server Logs"
56+
cat hugo.log
57+
echo "::endgroup::"
58+
fi
59+
60+
if [ -f /tmp/broken_links_report.json ]; then
61+
echo "::group::Broken Links Report"
62+
cat /tmp/broken_links_report.json
63+
echo "::endgroup::"
64+
fi
65+
66+
exit $exit_code
67+
}
5068
51-
# Run the validation
52-
if node cypress/support/run-e2e-specs.js \
53-
--spec "cypress/e2e/content/article-links.cy.js" \
54-
${{ inputs.files }}; then
55-
echo "failed=false" >> $GITHUB_OUTPUT
56-
else
57-
echo "failed=true" >> $GITHUB_OUTPUT
58-
exit 1
59-
fi
60-
shell: bash
61-
env:
62-
CI: true
63-
CACHE_ENABLED: ${{ inputs.cache-enabled }}
69+
- name: Upload logs on failure
70+
if: failure()
71+
uses: actions/upload-artifact@v4
72+
with:
73+
name: validation-logs-${{ inputs.product-name }}
74+
path: |
75+
hugo.log
6476
6577
- name: Upload broken links report
6678
if: failure()

cypress/support/run-e2e-specs.js

Lines changed: 94 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,13 @@ async function main() {
123123
console.log(`Performing cleanup before exit with code ${code}...`);
124124
if (hugoProc && hugoStarted) {
125125
try {
126-
hugoProc.kill('SIGKILL'); // Use SIGKILL to ensure immediate termination
126+
// Use SIGTERM first, then SIGKILL if needed
127+
hugoProc.kill('SIGTERM');
128+
setTimeout(() => {
129+
if (!hugoProc.killed) {
130+
hugoProc.kill('SIGKILL');
131+
}
132+
}, 1000);
127133
} catch (err) {
128134
console.error(`Error killing Hugo process: ${err.message}`);
129135
}
@@ -138,6 +144,10 @@ async function main() {
138144
console.error(`Uncaught exception: ${err.message}`);
139145
cleanupAndExit(1);
140146
});
147+
process.on('unhandledRejection', (reason, promise) => {
148+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
149+
cleanupAndExit(1);
150+
});
141151

142152
const { fileArgs, specArgs } = parseArgs(process.argv);
143153
if (fileArgs.length === 0) {
@@ -354,13 +364,24 @@ async function main() {
354364
initializeReport();
355365

356366
console.log(`Running Cypress tests for ${urlList.length} URLs...`);
367+
368+
// Add CI-specific configuration
369+
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
370+
357371
const cypressOptions = {
358372
reporter: 'junit',
359373
browser: 'chrome',
360374
config: {
361375
baseUrl: `http://localhost:${HUGO_PORT}`,
362-
video: true,
363-
trashAssetsBeforeRuns: false, // Prevent trash errors
376+
video: !isCI, // Disable video in CI to reduce resource usage
377+
trashAssetsBeforeRuns: false,
378+
// Add CI-specific timeouts
379+
defaultCommandTimeout: isCI ? 15000 : 10000,
380+
pageLoadTimeout: isCI ? 45000 : 30000,
381+
responseTimeout: isCI ? 45000 : 30000,
382+
// Reduce memory usage in CI
383+
experimentalMemoryManagement: true,
384+
numTestsKeptInMemory: isCI ? 1 : 5,
364385
},
365386
env: {
366387
// Pass URLs as a comma-separated string for backward compatibility
@@ -377,8 +398,28 @@ async function main() {
377398
cypressOptions.spec = specArgs.join(',');
378399
}
379400

401+
// Add error handling for Hugo process monitoring during Cypress execution
402+
let hugoHealthCheckInterval;
403+
if (hugoProc && hugoStarted) {
404+
hugoHealthCheckInterval = setInterval(() => {
405+
if (hugoProc.killed || hugoProc.exitCode !== null) {
406+
console.error('❌ Hugo server died during Cypress execution');
407+
if (hugoHealthCheckInterval) {
408+
clearInterval(hugoHealthCheckInterval);
409+
}
410+
cypressFailed = true;
411+
// Don't exit immediately, let Cypress finish gracefully
412+
}
413+
}, 5000);
414+
}
415+
380416
const results = await cypress.run(cypressOptions);
381417

418+
// Clear health check interval
419+
if (hugoHealthCheckInterval) {
420+
clearInterval(hugoHealthCheckInterval);
421+
}
422+
382423
// Process broken links report
383424
const brokenLinksCount = displayBrokenLinksReport();
384425

@@ -496,6 +537,22 @@ async function main() {
496537
} catch (err) {
497538
console.error(`❌ Cypress execution error: ${err.message}`);
498539

540+
// Handle EPIPE errors specifically
541+
if (err.code === 'EPIPE' || err.message.includes('EPIPE')) {
542+
console.error('🔧 EPIPE Error Detected:');
543+
console.error(' • This usually indicates the Hugo server process was terminated unexpectedly');
544+
console.error(' • Common causes in CI:');
545+
console.error(' - Memory constraints causing process termination');
546+
console.error(' - CI runner timeout or resource limits');
547+
console.error(' - Hugo server crash due to build errors');
548+
console.error(` • Check Hugo logs: ${HUGO_LOG_FILE}`);
549+
550+
// Try to provide more context about Hugo server state
551+
if (hugoProc) {
552+
console.error(` • Hugo process state: killed=${hugoProc.killed}, exitCode=${hugoProc.exitCode}`);
553+
}
554+
}
555+
499556
// Provide more detailed error information
500557
if (err.stack) {
501558
console.error('📋 Error Stack Trace:');
@@ -552,28 +609,44 @@ async function main() {
552609
if (hugoProc && hugoStarted && typeof hugoProc.kill === 'function') {
553610
console.log(`Stopping Hugo server (fast shutdown: ${cypressFailed})...`);
554611

555-
if (cypressFailed) {
556-
hugoProc.kill('SIGKILL');
557-
console.log('Hugo server forcibly terminated');
558-
} else {
559-
const shutdownTimeout = setTimeout(() => {
560-
console.error(
561-
'Hugo server did not shut down gracefully, forcing termination'
562-
);
612+
try {
613+
if (cypressFailed) {
614+
// Fast shutdown for failed tests
563615
hugoProc.kill('SIGKILL');
564-
process.exit(exitCode);
565-
}, 2000);
616+
console.log('Hugo server forcibly terminated');
617+
} else {
618+
// Graceful shutdown for successful tests
619+
const shutdownTimeout = setTimeout(() => {
620+
console.error(
621+
'Hugo server did not shut down gracefully, forcing termination'
622+
);
623+
try {
624+
hugoProc.kill('SIGKILL');
625+
} catch (killErr) {
626+
console.error(`Error force-killing Hugo: ${killErr.message}`);
627+
}
628+
process.exit(exitCode);
629+
}, 3000); // Increased timeout for CI
566630

567-
hugoProc.kill('SIGTERM');
631+
hugoProc.kill('SIGTERM');
568632

569-
hugoProc.on('close', () => {
570-
clearTimeout(shutdownTimeout);
571-
console.log('Hugo server shut down successfully');
572-
process.exit(exitCode);
573-
});
633+
hugoProc.on('close', () => {
634+
clearTimeout(shutdownTimeout);
635+
console.log('Hugo server shut down successfully');
636+
process.exit(exitCode);
637+
});
574638

575-
// Return to prevent immediate exit
576-
return;
639+
hugoProc.on('error', (err) => {
640+
console.error(`Error during Hugo shutdown: ${err.message}`);
641+
clearTimeout(shutdownTimeout);
642+
process.exit(exitCode);
643+
});
644+
645+
// Return to prevent immediate exit
646+
return;
647+
}
648+
} catch (shutdownErr) {
649+
console.error(`Error during Hugo server shutdown: ${shutdownErr.message}`);
577650
}
578651
} else if (hugoStarted) {
579652
console.log('Hugo process was started but is not available for cleanup');

0 commit comments

Comments
 (0)