Files
ReadMeABook/.github/workflows/run-tests.yml
T
kikootwo 428d9a12e0 Improve test output parsing in CI workflow
Strips ANSI color codes from test output for reliable parsing and updates grep logic to use the last relevant lines for test files, tests, and duration. This enhances robustness when extracting test statistics from vitest output in the GitHub Actions workflow.
2026-01-28 11:41:59 -05:00

260 lines
10 KiB
YAML

name: Backend Tests
on:
workflow_dispatch:
workflow_call:
inputs:
send_notification:
description: "Whether to send Discord notification"
type: boolean
default: true
secrets:
WEBHOOK_URL:
description: "Discord webhook URL"
required: false
outputs:
success:
description: "Whether tests passed"
value: ${{ jobs.test.outputs.success }}
total:
description: "Total number of tests"
value: ${{ jobs.test.outputs.total }}
passed:
description: "Number of passed tests"
value: ${{ jobs.test.outputs.passed }}
failed:
description: "Number of failed tests"
value: ${{ jobs.test.outputs.failed }}
duration:
description: "Test duration in seconds"
value: ${{ jobs.test.outputs.duration }}
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
outputs:
success: ${{ steps.test-results.outputs.success }}
total: ${{ steps.test-results.outputs.total }}
passed: ${{ steps.test-results.outputs.passed }}
failed: ${{ steps.test-results.outputs.failed }}
duration: ${{ steps.test-results.outputs.duration }}
test_files: ${{ steps.test-results.outputs.test_files }}
test_files_passed: ${{ steps.test-results.outputs.test_files_passed }}
test_files_failed: ${{ steps.test-results.outputs.test_files_failed }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
id: run-tests
continue-on-error: true
run: |
START_TIME=$(date +%s)
npm test 2>&1 | tee test-output.txt
TEST_EXIT_CODE=${PIPESTATUS[0]}
END_TIME=$(date +%s)
echo "exit_code=$TEST_EXIT_CODE" >> $GITHUB_OUTPUT
echo "start_time=$(date -u +"%H:%M:%S")" >> $GITHUB_OUTPUT
echo "elapsed=$((END_TIME - START_TIME))" >> $GITHUB_OUTPUT
- name: Parse test results
id: test-results
if: always()
run: |
# Parse the test output from vitest's default reporter
if [ -f test-output.txt ]; then
# Strip ANSI color codes for reliable parsing
sed -i 's/\x1b\[[0-9;]*m//g' test-output.txt
# Debug: show relevant lines
echo "=== Relevant test output lines ==="
grep -E "(Test Files|Tests|Duration)" test-output.txt | tail -5 || true
echo "==================================="
# Extract test file count (e.g., "Test Files 81 passed (81)")
TEST_FILES_LINE=$(grep "Test Files" test-output.txt | tail -1 || echo "")
if [ -n "$TEST_FILES_LINE" ]; then
TEST_FILES_PASSED=$(echo "$TEST_FILES_LINE" | grep -oE "[0-9]+ passed" | grep -oE "[0-9]+" | head -1 || echo "0")
TEST_FILES_FAILED=$(echo "$TEST_FILES_LINE" | grep -oE "[0-9]+ failed" | grep -oE "[0-9]+" | head -1 || echo "0")
TEST_FILES=$(echo "$TEST_FILES_LINE" | grep -oE "\([0-9]+\)" | grep -oE "[0-9]+" | head -1 || echo "0")
else
TEST_FILES_PASSED=0
TEST_FILES_FAILED=0
TEST_FILES=0
fi
# Extract test count - look for line containing "Tests" followed by number, exclude "Test Files"
TESTS_LINE=$(grep -E "Tests\s+[0-9]+" test-output.txt | grep -v "Test Files" | tail -1 || echo "")
if [ -n "$TESTS_LINE" ]; then
PASSED=$(echo "$TESTS_LINE" | grep -oE "[0-9]+ passed" | grep -oE "[0-9]+" | head -1 || echo "0")
FAILED=$(echo "$TESTS_LINE" | grep -oE "[0-9]+ failed" | grep -oE "[0-9]+" | head -1 || echo "0")
TOTAL=$(echo "$TESTS_LINE" | grep -oE "\([0-9]+\)" | grep -oE "[0-9]+" | head -1 || echo "0")
else
PASSED=0
FAILED=0
TOTAL=0
fi
# Extract duration (e.g., "Duration 26.97s")
DURATION_LINE=$(grep -E "Duration\s+[0-9]+" test-output.txt | tail -1 || echo "")
if [ -n "$DURATION_LINE" ]; then
DURATION=$(echo "$DURATION_LINE" | grep -oE "[0-9]+\.[0-9]+s" | head -1 | sed 's/s//' || echo "0")
else
DURATION="${{ steps.run-tests.outputs.elapsed }}"
fi
# Determine success based on exit code and failed count
if [ "${{ steps.run-tests.outputs.exit_code }}" = "0" ] && [ "${FAILED:-0}" = "0" ]; then
SUCCESS="true"
else
SUCCESS="false"
fi
else
TOTAL=0
PASSED=0
FAILED=1
TEST_FILES=0
TEST_FILES_PASSED=0
TEST_FILES_FAILED=1
DURATION=0
SUCCESS="false"
fi
# Set defaults for empty values
TOTAL=${TOTAL:-0}
PASSED=${PASSED:-0}
FAILED=${FAILED:-0}
TEST_FILES=${TEST_FILES:-0}
TEST_FILES_PASSED=${TEST_FILES_PASSED:-0}
TEST_FILES_FAILED=${TEST_FILES_FAILED:-0}
DURATION=${DURATION:-0}
echo "=== Parsed values ==="
echo "TEST_FILES_PASSED=$TEST_FILES_PASSED"
echo "TEST_FILES=$TEST_FILES"
echo "PASSED=$PASSED"
echo "TOTAL=$TOTAL"
echo "DURATION=$DURATION"
echo "SUCCESS=$SUCCESS"
echo "===================="
echo "total=$TOTAL" >> $GITHUB_OUTPUT
echo "passed=$PASSED" >> $GITHUB_OUTPUT
echo "failed=$FAILED" >> $GITHUB_OUTPUT
echo "duration=$DURATION" >> $GITHUB_OUTPUT
echo "success=$SUCCESS" >> $GITHUB_OUTPUT
echo "test_files=$TEST_FILES" >> $GITHUB_OUTPUT
echo "test_files_passed=$TEST_FILES_PASSED" >> $GITHUB_OUTPUT
echo "test_files_failed=$TEST_FILES_FAILED" >> $GITHUB_OUTPUT
echo "## 🧪 Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "$SUCCESS" = "true" ]; then
echo "### ✅ All tests passed!" >> $GITHUB_STEP_SUMMARY
else
echo "### ❌ Some tests failed" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY
echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
echo "| Test Files | $TEST_FILES_PASSED passed ($TEST_FILES) |" >> $GITHUB_STEP_SUMMARY
echo "| Tests | $PASSED passed ($TOTAL) |" >> $GITHUB_STEP_SUMMARY
echo "| Duration | ${DURATION}s |" >> $GITHUB_STEP_SUMMARY
- name: Send Discord notification
if: always() && (inputs.send_notification != false)
env:
WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }}
run: |
if [ -z "$WEBHOOK_URL" ]; then
echo "No webhook URL provided, skipping notification"
exit 0
fi
SUCCESS="${{ steps.test-results.outputs.success }}"
TOTAL="${{ steps.test-results.outputs.total }}"
PASSED="${{ steps.test-results.outputs.passed }}"
FAILED="${{ steps.test-results.outputs.failed }}"
DURATION="${{ steps.test-results.outputs.duration }}"
TEST_FILES="${{ steps.test-results.outputs.test_files }}"
TEST_FILES_PASSED="${{ steps.test-results.outputs.test_files_passed }}"
TEST_FILES_FAILED="${{ steps.test-results.outputs.test_files_failed }}"
if [ "$SUCCESS" = "true" ]; then
COLOR=5763719
TITLE="✅ Tests Passed"
DESCRIPTION="All tests completed successfully for **ReadMeABook**"
TEST_FILES_VALUE="$TEST_FILES_PASSED passed ($TEST_FILES)"
TESTS_VALUE="$PASSED passed ($TOTAL)"
else
COLOR=15548997
TITLE="❌ Tests Failed"
DESCRIPTION="Some tests failed for **ReadMeABook**"
TEST_FILES_VALUE="$TEST_FILES_PASSED passed, $TEST_FILES_FAILED failed ($TEST_FILES)"
TESTS_VALUE="$PASSED passed, $FAILED failed ($TOTAL)"
fi
curl -H "Content-Type: application/json" \
-X POST \
-d "{
\"embeds\": [{
\"title\": \"$TITLE\",
\"description\": \"$DESCRIPTION\",
\"color\": $COLOR,
\"fields\": [
{
\"name\": \"📁 Test Files\",
\"value\": \"\`$TEST_FILES_VALUE\`\",
\"inline\": true
},
{
\"name\": \"🧪 Tests\",
\"value\": \"\`$TESTS_VALUE\`\",
\"inline\": true
},
{
\"name\": \"⏱️ Duration\",
\"value\": \"\`${DURATION}s\`\",
\"inline\": true
},
{
\"name\": \"🌿 Branch\",
\"value\": \"\`${{ github.ref_name }}\`\",
\"inline\": true
},
{
\"name\": \"📋 Commit\",
\"value\": \"[\`$(echo ${{ github.sha }} | cut -c1-7)\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})\",
\"inline\": true
},
{
\"name\": \"🔗 Workflow\",
\"value\": \"[View Run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})\",
\"inline\": true
}
],
\"footer\": {
\"text\": \"ReadMeABook CI/CD • Test Suite\"
},
\"timestamp\": \"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\"
}]
}" \
"$WEBHOOK_URL"
- name: Fail if tests failed
if: steps.test-results.outputs.success != 'true'
run: exit 1