Skip to content

Development: Rework user data export server side #184

Development: Rework user data export server side

Development: Rework user data export server side #184

name: PR Coverage Reporter
on:
pull_request:
types: [ opened, synchronize, ready_for_review ]
# Only one coverage run per PR at a time
concurrency:
group: coverage-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
CI: true
node: 24
java: 25
jobs:
detect-changes:
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
has_client_changes: ${{ steps.changes.outputs.has_client_changes }}
has_server_changes: ${{ steps.changes.outputs.has_server_changes }}
client_modules: ${{ steps.changes.outputs.client_modules }}
server_modules: ${{ steps.changes.outputs.server_modules }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Detect changed modules
id: changes
run: |
# Get the base branch
BASE_BRANCH="${{ github.event.pull_request.base.ref }}"
git fetch origin "$BASE_BRANCH"
# Get changed files
CHANGED_FILES=$(git diff --name-only "origin/$BASE_BRANCH"...HEAD)
# Detect client changes and modules
CLIENT_MODULES=""
HAS_CLIENT_CHANGES="false"
for file in $CHANGED_FILES; do
if [[ "$file" == src/main/webapp/app/*.ts ]] && [[ "$file" != *.spec.ts ]] && [[ "$file" != *.module.ts ]]; then
HAS_CLIENT_CHANGES="true"
# Extract module name (first directory after app/)
MODULE=$(echo "$file" | sed -n 's|^src/main/webapp/app/\([^/]*\)/.*|\1|p')
if [ -n "$MODULE" ] && [[ ! "$CLIENT_MODULES" =~ (^|,)$MODULE(,|$) ]]; then
if [ -z "$CLIENT_MODULES" ]; then
CLIENT_MODULES="$MODULE"
else
CLIENT_MODULES="$CLIENT_MODULES,$MODULE"
fi
fi
fi
done
# Detect server changes and modules
SERVER_MODULES=""
HAS_SERVER_CHANGES="false"
for file in $CHANGED_FILES; do
if [[ "$file" == src/main/java/de/tum/cit/aet/*.java ]]; then
HAS_SERVER_CHANGES="true"
# Extract module name (first directory after aet/)
MODULE=$(echo "$file" | sed -n 's|^src/main/java/de/tum/cit/aet/\([^/]*\)/.*|\1|p')
if [ -n "$MODULE" ] && [[ ! "$SERVER_MODULES" =~ (^|,)$MODULE(,|$) ]]; then
if [ -z "$SERVER_MODULES" ]; then
SERVER_MODULES="$MODULE"
else
SERVER_MODULES="$SERVER_MODULES,$MODULE"
fi
fi
fi
done
echo "has_client_changes=$HAS_CLIENT_CHANGES" >> $GITHUB_OUTPUT
echo "has_server_changes=$HAS_SERVER_CHANGES" >> $GITHUB_OUTPUT
echo "client_modules=$CLIENT_MODULES" >> $GITHUB_OUTPUT
echo "server_modules=$SERVER_MODULES" >> $GITHUB_OUTPUT
echo "Client changes: $HAS_CLIENT_CHANGES (modules: $CLIENT_MODULES)"
echo "Server changes: $HAS_SERVER_CHANGES (modules: $SERVER_MODULES)"
update-no-coverage-needed:
needs: detect-changes
if: needs.detect-changes.outputs.has_client_changes == 'false' && needs.detect-changes.outputs.has_server_changes == 'false'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: write
steps:
- name: Update PR description
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
let body = pr.body || '';
const coverageSection = '### Test Coverage';
const coverageIndex = body.indexOf(coverageSection);
if (coverageIndex === -1) {
console.log('Test Coverage section not found in PR description');
return;
}
const afterCoverage = body.substring(coverageIndex + coverageSection.length);
const nextSectionMatch = afterCoverage.match(/\n###\s/);
const nextSectionIndex = nextSectionMatch
? coverageIndex + coverageSection.length + nextSectionMatch.index
: body.length;
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
// Build content using array join to avoid YAML parsing issues
const lines = [
coverageSection,
'<!-- Please add the test coverages for all changed files modified in this PR here. You can generate the coverage table using one of these options: -->',
'<!-- 1. Run `npm run coverage:pr` to generate coverage locally by running only the affected module tests (see supporting_scripts/code-coverage/local-pr-coverage/README.md) -->',
'<!-- 2. Use `supporting_scripts/code-coverage/generate_code_cov_table/generate_code_cov_table.py` to generate the table from CI artifacts (requires GitHub token, follow the README for setup) -->',
'<!-- The line coverage must be above 90% for changed files, and you must use extensive and useful assertions for server tests and expect statements for client tests. -->',
'<!-- Note: Confirm in the last column that you have implemented extensive assertions for server tests and expect statements for client tests. -->',
'<!-- Remove rows with only trivial changes from the table. -->',
'',
'**No code changes detected** - test coverage not required for this PR.',
'',
`_Last updated: ${timestamp}_`,
'',
''
];
const newCoverageContent = lines.join('\n');
const newBody = body.substring(0, coverageIndex) + newCoverageContent + body.substring(nextSectionIndex);
await github.rest.pulls.update({
owner,
repo,
pull_number: prNumber,
body: newBody
});
console.log('Updated PR description with no-coverage-needed message');
client-coverage:
needs: detect-changes
if: needs.detect-changes.outputs.has_client_changes == 'true'
runs-on: aet-large-ubuntu
timeout-minutes: 45
outputs:
success: ${{ steps.run-tests.outputs.success }}
coverage_table: ${{ steps.run-tests.outputs.coverage_table }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '${{ env.node }}'
cache: 'npm'
- name: Install Dependencies
run: npm install
- name: Run client tests and generate coverage
id: run-tests
run: |
set -o pipefail
MODULES="${{ needs.detect-changes.outputs.client_modules }}"
if [ -n "$MODULES" ]; then
echo "Running client tests for modules: $MODULES"
COVERAGE_OUTPUT=$(npm run coverage:pr -- --client-only --client-modules "$MODULES" --print 2>&1) || {
echo "success=false" >> $GITHUB_OUTPUT
echo "Tests failed"
exit 0
}
else
echo "Running all client tests"
COVERAGE_OUTPUT=$(npm run coverage:pr -- --client-only --print 2>&1) || {
echo "success=false" >> $GITHUB_OUTPUT
echo "Tests failed"
exit 0
}
fi
echo "success=true" >> $GITHUB_OUTPUT
# Extract just the coverage table (between the separator lines)
COVERAGE_TABLE=$(echo "$COVERAGE_OUTPUT" | sed -n '/^─/,/^─/p' | sed '1d;$d')
# Use heredoc to handle multiline output
{
echo 'coverage_table<<EOF'
echo "$COVERAGE_TABLE"
echo 'EOF'
} >> $GITHUB_OUTPUT
- name: Upload client coverage artifacts
if: always()
uses: actions/upload-artifact@v6
with:
name: client-coverage-report
path: |
build/test-results/jest/coverage-summary.json
build/test-results/vitest/coverage/coverage-summary.json
if-no-files-found: ignore
server-coverage:
needs: detect-changes
if: needs.detect-changes.outputs.has_server_changes == 'true'
runs-on: aet-large-ubuntu
timeout-minutes: 80
outputs:
success: ${{ steps.run-tests.outputs.success }}
coverage_table: ${{ steps.run-tests.outputs.coverage_table }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: |
17
${{ env.java }}
cache: 'gradle'
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '${{ env.node }}'
cache: 'npm'
- name: Install Dependencies
run: npm install
- name: Run server tests and generate coverage
id: run-tests
run: |
set -o pipefail
MODULES="${{ needs.detect-changes.outputs.server_modules }}"
if [ -n "$MODULES" ]; then
echo "Running server tests for modules: $MODULES"
COVERAGE_OUTPUT=$(npm run coverage:pr -- --server-only --server-modules "$MODULES" --print 2>&1) || {
echo "success=false" >> $GITHUB_OUTPUT
echo "Tests failed"
exit 0
}
else
echo "Running all server tests"
COVERAGE_OUTPUT=$(npm run coverage:pr -- --server-only --print 2>&1) || {
echo "success=false" >> $GITHUB_OUTPUT
echo "Tests failed"
exit 0
}
fi
echo "success=true" >> $GITHUB_OUTPUT
# Extract just the coverage table (between the separator lines)
COVERAGE_TABLE=$(echo "$COVERAGE_OUTPUT" | sed -n '/^─/,/^─/p' | sed '1d;$d')
# Use heredoc to handle multiline output
{
echo 'coverage_table<<EOF'
echo "$COVERAGE_TABLE"
echo 'EOF'
} >> $GITHUB_OUTPUT
- name: Upload server coverage artifacts
if: always()
uses: actions/upload-artifact@v6
with:
name: server-coverage-report
path: build/reports/jacoco/
if-no-files-found: ignore
update-pr-coverage:
needs: [ detect-changes, client-coverage, server-coverage ]
if: always() && (needs.detect-changes.outputs.has_client_changes == 'true' || needs.detect-changes.outputs.has_server_changes == 'true')
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: write
steps:
- name: Update PR description with coverage
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const prNumber = context.payload.pull_request.number;
const owner = context.repo.owner;
const repo = context.repo.repo;
const author = context.payload.pull_request.user.login;
const hasClientChanges = '${{ needs.detect-changes.outputs.has_client_changes }}' === 'true';
const hasServerChanges = '${{ needs.detect-changes.outputs.has_server_changes }}' === 'true';
// Handle skipped jobs - outputs will be empty strings
const clientSuccessStr = '${{ needs.client-coverage.outputs.success }}';
const serverSuccessStr = '${{ needs.server-coverage.outputs.success }}';
const clientSuccess = clientSuccessStr === 'true';
const serverSuccess = serverSuccessStr === 'true';
const clientTable = `${{ needs.client-coverage.outputs.coverage_table }}`;
const serverTable = `${{ needs.server-coverage.outputs.coverage_table }}`;
// Determine failures (only count as failed if the job ran)
const clientFailed = hasClientChanges && clientSuccessStr !== '' && !clientSuccess;
const serverFailed = hasServerChanges && serverSuccessStr !== '' && !serverSuccess;
const anyFailed = clientFailed || serverFailed;
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: prNumber
});
let body = pr.body || '';
const coverageSection = '### Test Coverage';
const coverageIndex = body.indexOf(coverageSection);
if (coverageIndex === -1) {
console.log('Test Coverage section not found in PR description');
return;
}
const afterCoverage = body.substring(coverageIndex + coverageSection.length);
const nextSectionMatch = afterCoverage.match(/\n###\s/);
const nextSectionIndex = nextSectionMatch
? coverageIndex + coverageSection.length + nextSectionMatch.index
: body.length;
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
const runUrl = `https://github.com/${owner}/${repo}/actions/runs/${{ github.run_id }}`;
// Build content using array
const lines = [
coverageSection,
'<!-- Please add the test coverages for all changed files modified in this PR here. You can generate the coverage table using one of these options: -->',
'<!-- 1. Run `npm run coverage:pr` to generate coverage locally by running only the affected module tests (see supporting_scripts/code-coverage/local-pr-coverage/README.md) -->',
'<!-- 2. Use `supporting_scripts/code-coverage/generate_code_cov_table/generate_code_cov_table.py` to generate the table from CI artifacts (requires GitHub token, follow the README for setup) -->',
'<!-- The line coverage must be above 90% for changed files, and you must use extensive and useful assertions for server tests and expect statements for client tests. -->',
'<!-- Note: Confirm in the last column that you have implemented extensive assertions for server tests and expect statements for client tests. -->',
'<!-- Remove rows with only trivial changes from the table. -->',
''
];
if (anyFailed) {
let warningMsg = '**Warning:** ';
if (clientFailed && serverFailed) {
warningMsg += 'Both client and server tests failed. ';
} else if (clientFailed) {
warningMsg += 'Client tests failed. ';
} else {
warningMsg += 'Server tests failed. ';
}
warningMsg += `Coverage could not be fully measured. Please check the [workflow logs](${runUrl}).`;
lines.push(warningMsg, '');
}
// Add tables if available
if (hasClientChanges && clientTable && clientTable.trim()) {
lines.push(clientTable, '');
}
if (hasServerChanges && serverTable && serverTable.trim()) {
lines.push(serverTable, '');
}
lines.push(`_Last updated: ${timestamp}_`, '', '');
const newCoverageContent = lines.join('\n');
const newBody = body.substring(0, coverageIndex) + newCoverageContent + body.substring(nextSectionIndex);
await github.rest.pulls.update({
owner,
repo,
pull_number: prNumber,
body: newBody
});
console.log('Updated PR description with coverage results');
// Post a comment notifying the author
let commentBody;
if (anyFailed) {
commentBody = `@${author} Test coverage could not be fully measured because some tests failed. Please check the [workflow logs](${runUrl}) for details.`;
} else {
commentBody = `@${author} Test coverage has been automatically updated in the PR description.`;
}
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: commentBody
});