Skip to content

Commit ab6718c

Browse files
committed
ci: refactor grade calculation
- Replace percentage scores with marks out of 5 - Disable execution in template repos - Apply formatting
1 parent 6ca1abb commit ab6718c

File tree

1 file changed

+46
-74
lines changed

1 file changed

+46
-74
lines changed

.github/workflows/classroom-autograding.yml

Lines changed: 46 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ env:
99
GRADING_REPO: cbfacademy/autograding-java-exercises
1010
GRADING_REPO_PATH: grading-repo
1111
STUDENT_REPO_PATH: student-repo
12-
MAX_TESTS: ${{ vars.MAX_TESTS || 42 }}
13-
TEST_SCORE_MARKS: ${{ vars.TEST_SCORE_MARKS || 70 }}
14-
AGENT_SCORE_MARKS: ${{ vars.AGENT_SCORE_MARKS || 30 }}
12+
MAX_TESTS: 42
1513

1614
jobs:
1715
autograding:
16+
if: ${{ !github.event.repository.is_template && github.event.sender.type != 'Bot' }}
1817
runs-on: ubuntu-latest
19-
if: github.event.sender.type != 'Bot'
2018
steps:
2119
- name: Checkout grading repo
2220
uses: actions/checkout@v4
@@ -37,12 +35,12 @@ jobs:
3735
uses: actions/setup-java@v4
3836
with:
3937
distribution: temurin
40-
java-version: '21'
38+
java-version: "21"
4139
cache: maven
4240

4341
- uses: actions/setup-node@v3
4442
with:
45-
node-version: '20'
43+
node-version: "20"
4644
- run: npm install xml2js
4745

4846
- name: Extract modules from pom.xml
@@ -57,7 +55,7 @@ jobs:
5755
const parser = new xml2js.Parser();
5856
const result = await parser.parseStringPromise(pomXml);
5957
const modules = result.project.modules[0].module || [];
60-
58+
6159
core.setOutput('modules', modules.join(' '));
6260
6361
return modules.join(' ');
@@ -297,67 +295,57 @@ jobs:
297295
uses: actions/github-script@v7
298296
with:
299297
script: |
300-
// Helper function to parse result string
301-
function parseResult(result, scaleTo) {
298+
// Helper function to parse result string and scale to 5
299+
function parseResult(result) {
302300
const [earnedStr, totalStr] = result.replace(/^"|"$/g, '').split('/');
303301
let earned = parseInt(earnedStr, 10);
304302
let available = parseInt(totalStr, 10);
305303
306-
if (scaleTo !== undefined && available > 0) {
307-
earned = Math.round((earned / available) * scaleTo);
308-
available = scaleTo;
304+
if (available > 0) {
305+
earned = Math.round((earned / available) * 5);
306+
} else {
307+
earned = 0;
309308
}
310-
311-
return { earned, available };
309+
return earned;
312310
}
313311
314-
// Parse get-tests-score result (e.g., 34/42), scale to 70%
315-
const testsResult = '${{ steps.get-tests-score.outputs.result }}';
316-
const tests = parseResult(testsResult, ${{ env.TEST_SCORE_MARKS }});
317-
// Parse get-agent-score result (e.g., 27/100), scale to 30%
318-
const aiResult = '${{ steps.get-agent-score.outputs.result }}';
319-
const ai = parseResult(aiResult, ${{ env.AGENT_SCORE_MARKS }});
320-
// Sum
321-
const totalEarned = tests.earned + ai.earned;
322-
const totalAvailable = tests.available + ai.available;
323-
324-
console.log(`Tests score: ${JSON.stringify(tests)}`);
325-
console.log(`Agent score: ${JSON.stringify(ai)}`);
326-
core.setOutput('earned', totalEarned);
327-
core.setOutput('available', totalAvailable);
312+
// Parse get-tests-score and get-agent-score results (e.g., 34/42, 27/100)
313+
const testsScore = parseResult('${{ steps.get-tests-score.outputs.result }}');
314+
const agentScore = parseResult('${{ steps.get-agent-score.outputs.result }}');
315+
// Total out of 10
316+
const totalScore = testsScore + agentScore;
317+
318+
console.log(`Functionality (out of 5): ${testsScore}`);
319+
console.log(`Code Quality (out of 5): ${agentScore}`);
320+
console.log(`Total (out of 10): ${totalScore}`);
321+
core.setOutput('functionality', testsScore);
322+
core.setOutput('code_quality', agentScore);
323+
core.setOutput('earned', totalScore);
324+
core.setOutput('available', 10);
328325
329326
- name: Trigger Airtable update
330327
uses: actions/github-script@v7
331328
env:
332329
GRADING_REPO: ${{ env.GRADING_REPO }}
333330
REPO_URL: ${{ github.repository }}
334-
TEST_SCORE: ${{ steps.get-tests-score.outputs.passed }}
335-
AGENT_SCORE: ${{ steps.get-agent-score.outputs.result }}
331+
TEST_SCORE: ${{ steps.aggregate-points.outputs.functionality }}
332+
AGENT_SCORE: ${{ steps.aggregate-points.outputs.code_quality }}
336333
with:
337334
github-token: ${{ secrets.CLASSROOM_TOKEN }}
338335
script: |
339336
const [owner, repo] = process.env.GRADING_REPO.split('/');
340-
341-
// Convert agent score from percentage to 0-5 scale
342-
const agentScorePercent = parseInt(process.env.AGENT_SCORE, 10);
343-
const agentScore = Math.round((agentScorePercent / 100) * 5);
344-
345-
// Convert test score to 0-5 scale
346-
const testScore = Math.round((parseInt(process.env.TEST_SCORE, 10) / ${{ env.MAX_TESTS }}) * 5);
347-
348337
// Ensure repository URL has trailing slash
349338
const repoUrl = process.env.REPO_URL.endsWith('/') ? process.env.REPO_URL : `${process.env.REPO_URL}/`;
350-
351339
try {
352340
await github.rest.actions.createWorkflowDispatch({
353341
owner,
354342
repo,
355343
workflow_id: 'update-airtable.yml',
356344
ref: 'main',
357345
inputs: {
358-
test_score: testScore.toString(),
359-
agent_score: agentScore.toString(),
360-
repository_url: repoUrl
346+
test_score: process.env.TEST_SCORE,
347+
agent_score: process.env.AGENT_SCORE,
348+
repository_url: `https://github.com/${repoUrl}`
361349
}
362350
});
363351
console.log('Successfully triggered Airtable update workflow');
@@ -377,6 +365,8 @@ jobs:
377365
EARNED: ${{ steps.aggregate-points.outputs.earned }}
378366
AVAILABLE: ${{ steps.aggregate-points.outputs.available }}
379367
BOT_USER: ${{ vars.PR_AGENT_BOT_USER }}
368+
FUNCTIONALITY: ${{ steps.aggregate-points.outputs.functionality }}
369+
CODE_QUALITY: ${{ steps.aggregate-points.outputs.code_quality }}
380370
with:
381371
github-token: ${{ secrets.CLASSROOM_TOKEN }}
382372
script: |
@@ -396,10 +386,8 @@ jobs:
396386
}
397387
398388
function calculateGrade(earned, available) {
399-
const earnedNum = parseInt(earned, 10);
400-
const availableNum = parseInt(available, 10);
401-
const percent = availableNum > 0 ? (earnedNum / availableNum) * 100 : 0;
402-
389+
// Calculate grade based on marks out of 10
390+
const percent = available > 0 ? (earned / available) * 100 : 0;
403391
if (percent >= 90) return 'A+';
404392
if (percent >= 80) return 'A';
405393
if (percent >= 70) return 'B';
@@ -409,33 +397,21 @@ jobs:
409397
return 'Fail';
410398
}
411399
412-
function calculateFunctionalityScore(passed, total) {
413-
const passedNum = parseInt(passed, 10);
414-
const totalNum = parseInt(total, 10);
415-
416-
return totalNum > 0 ? Math.round((passedNum / totalNum) * 100) : 0;
417-
}
418-
419-
function formatBody({ score, comment, gradingRepo, gradingPrId, studentRepo, feedbackPr, passed, total, earned, available }) {
420-
const funcScore = calculateFunctionalityScore(passed, total);
400+
function formatBody({ score, comment, gradingRepo, gradingPrId, studentRepo, feedbackPr, functionality, codeQuality, earned, available }) {
421401
const grade = calculateGrade(earned, available);
422402
let fixedBody = comment.replace(new RegExp(`${gradingRepo}/pull/${gradingPrId}`, 'g'), `${studentRepo}/pull/${feedbackPr}`);
423403
424404
fixedBody = fixedBody.replace(/PR Reviewer Guide/g, 'Exercise Status Report');
425405
fixedBody = fixedBody.replace(/to aid the review process/g, 'on the current state of your exercise');
426406
427-
const qualityScoreRowRegex = /<tr><td>🏅&nbsp;<strong>Score<\/strong>: (\d+)<\/td><\/tr>/;
407+
// Replace the score table with individual marks out of 5 and overall grade
408+
const scoreTableRegex = /<tr><td>🏅&nbsp;<strong>Score<\/strong>: (\d+)<\/td><\/tr>/;
428409
const rowTemplate = (label, value) => `<tr><td>${label}: <strong>${value}</strong></td></tr>`;
429-
const match = fixedBody.match(qualityScoreRowRegex);
430-
431-
if (match) {
432-
const qualityScore = match[1];
433-
const funcRow = rowTemplate('Functionality', `${funcScore}%`);
434-
const qualityRow = rowTemplate('Code Quality', `${qualityScore}%`);
435-
const gradeRow = rowTemplate('Overall Grade', grade);
410+
const funcRow = rowTemplate('Functionality', `${functionality}/5`);
411+
const qualityRow = rowTemplate('Code Quality', `${codeQuality}/5`);
412+
const gradeRow = rowTemplate('Overall Grade', grade);
436413
437-
fixedBody = fixedBody.replace(qualityScoreRowRegex, `${funcRow}\n${qualityRow}\n${gradeRow}`);
438-
}
414+
fixedBody = fixedBody.replace(scoreTableRegex, `${funcRow}\n${qualityRow}\n${gradeRow}`);
439415
440416
return fixedBody;
441417
}
@@ -460,10 +436,10 @@ jobs:
460436
const gradingRepo = process.env.GRADING_REPO;
461437
const gradingPrId = process.env.GRADING_PR_ID;
462438
const studentRepo = process.env.STUDENT_REPO;
463-
const passed = process.env.PASSED;
464-
const total = process.env.TOTAL;
465-
const earned = process.env.EARNED;
466-
const available = process.env.AVAILABLE;
439+
const functionality = parseFloat(process.env.FUNCTIONALITY);
440+
const codeQuality = parseFloat(process.env.CODE_QUALITY);
441+
const earned = parseFloat(process.env.EARNED);
442+
const available = parseFloat(process.env.AVAILABLE);
467443
const botUser = process.env.BOT_USER;
468444
const [owner, repo] = gradingRepo.split('/');
469445
const [studentOwner, studentRepoName] = studentRepo.split('/');
@@ -473,7 +449,6 @@ jobs:
473449
feedbackPr = await getFeedbackPrNumber({ github, owner: studentOwner, repo: studentRepoName });
474450
} catch (err) {
475451
core.setFailed(`Failed to find feedback PR: ${err.message}`);
476-
477452
return;
478453
}
479454
@@ -483,17 +458,15 @@ jobs:
483458
bodyRaw = await getBotCommentBody({ github, owner, repo, issue_number: gradingPrId, botUser });
484459
} catch (err) {
485460
core.setFailed(`Failed to fetch bot comment: ${err.message}`);
486-
487461
return;
488462
}
489463
490464
let feedbackBody;
491465
492466
try {
493-
feedbackBody = formatBody({ score, comment: bodyRaw, gradingRepo, gradingPrId, studentRepo, feedbackPr, passed, total, earned, available });
467+
feedbackBody = formatBody({ score, comment: bodyRaw, gradingRepo, gradingPrId, studentRepo, feedbackPr, functionality, codeQuality, earned, available });
494468
} catch (err) {
495469
core.setFailed(`Failed to format feedback body: ${err.message}`);
496-
497470
return;
498471
}
499472
@@ -508,7 +481,6 @@ jobs:
508481
});
509482
} catch (err) {
510483
core.setFailed(`Failed to post feedback comment: ${err.message}`);
511-
512484
return;
513485
}
514486

0 commit comments

Comments
 (0)