|
| 1 | +name: Autograding Tests |
| 2 | +'on': |
| 3 | + - push |
| 4 | + - repository_dispatch |
| 5 | + |
| 6 | +permissions: |
| 7 | + checks: write |
| 8 | + actions: read |
| 9 | + contents: read |
| 10 | + pull-requests: write |
| 11 | + |
| 12 | +jobs: |
| 13 | + ai_feedback: |
| 14 | + name: AI-Powered Feedback |
| 15 | + runs-on: ubuntu-latest |
| 16 | + env: |
| 17 | + OPENROUTER_MODEL: ${{ vars.OPENROUTER_MODEL }} |
| 18 | + SYSTEM_PROMPT: ${{ vars.SYSTEM_PROMPT }} |
| 19 | + steps: |
| 20 | + - name: Checkout repository |
| 21 | + uses: actions/checkout@v5 |
| 22 | + |
| 23 | + - name: Read assignment instructions |
| 24 | + id: instructions |
| 25 | + run: | |
| 26 | + # Reads the content of the README.md file into an output variable. |
| 27 | + # The `EOF` marker is used to handle multi-line file content. |
| 28 | + echo "instructions=$(cat README.md | sed 's/\"/\\\"/g' | sed 's/$/\\n/' | tr -d '\n' | sed 's/\\n/\\\\n/g')" >> $GITHUB_OUTPUT |
| 29 | +
|
| 30 | + - name: Read source code |
| 31 | + id: source_code |
| 32 | + run: | |
| 33 | + { |
| 34 | + echo 'source_code<<EOF' |
| 35 | + find src/main/java -type f -name "*.java" | while read -r file; do |
| 36 | + echo "=== File: $file ===" |
| 37 | + cat "$file" |
| 38 | + echo |
| 39 | + done |
| 40 | + echo 'EOF' |
| 41 | + } >> "$GITHUB_OUTPUT" |
| 42 | +
|
| 43 | + - name: Read test code |
| 44 | + id: test_code |
| 45 | + run: | |
| 46 | + { |
| 47 | + echo 'test_code<<EOF' |
| 48 | + if [ -d "src/test/java" ]; then |
| 49 | + find src/test/java -type f -name "*.java" | while read -r file; do |
| 50 | + echo "=== File: $file ===" |
| 51 | + cat "$file" |
| 52 | + echo |
| 53 | + done |
| 54 | + else |
| 55 | + echo "No test code found." |
| 56 | + fi |
| 57 | + echo 'EOF' |
| 58 | + } >> "$GITHUB_OUTPUT" |
| 59 | + - name: Generate AI Feedback |
| 60 | + id: ai_feedback |
| 61 | + run: | |
| 62 | + # This step sends the collected data to the OpenRouter API. |
| 63 | + INSTRUCTIONS=$(jq -Rs . <<'EOF' |
| 64 | + ${{ steps.instructions.outputs.instructions }} |
| 65 | + EOF |
| 66 | + ) |
| 67 | + SOURCE_CODE=$(jq -Rs . <<'EOF' |
| 68 | + ${{ steps.source_code.outputs.source_code }} |
| 69 | + EOF |
| 70 | + ) |
| 71 | + TEST_CODE=$(jq -Rs . <<'EOF' |
| 72 | + ${{ steps.test_code.outputs.test_code }} |
| 73 | + EOF |
| 74 | + ) |
| 75 | +
|
| 76 | + if [ -z "$INSTRUCTIONS" ] || [ -z "$SOURCE_CODE" ] || [ -z "$TEST_CODE" ]; then |
| 77 | + echo "Error: One or more required variables are not set." |
| 78 | + exit 1 |
| 79 | + fi |
| 80 | +
|
| 81 | + # Assigning to USER_CONTENT with variable expansion |
| 82 | + PAYLOAD="Please provide feedback on the following Java assignment. |
| 83 | +
|
| 84 | + --- Assignment Instructions --- |
| 85 | + ${INSTRUCTIONS} |
| 86 | +
|
| 87 | + --- Source files --- |
| 88 | + ${SOURCE_CODE} |
| 89 | +
|
| 90 | + --- Test files --- |
| 91 | + ${TEST_CODE}" |
| 92 | +
|
| 93 | + JSON_CONTENT=$(jq -n \ |
| 94 | + --argjson model "$OPENROUTER_MODEL" \ |
| 95 | + --arg system_prompt "$SYSTEM_PROMPT" \ |
| 96 | + --arg payload "$PAYLOAD" \ |
| 97 | + '{ |
| 98 | + models: $model, |
| 99 | + messages: [ |
| 100 | + {role: "system", content: $system_prompt}, |
| 101 | + {role: "user", content: $payload} |
| 102 | + ] |
| 103 | + }') |
| 104 | +
|
| 105 | + echo "$JSON_CONTENT" |
| 106 | +
|
| 107 | + API_RESPONSE=$(echo "$JSON_CONTENT" | curl https://openrouter.ai/api/v1/chat/completions \ |
| 108 | + -H "Authorization: Bearer ${{ secrets.OPENROUTER_API_KEY }}" \ |
| 109 | + -H "Content-Type: application/json" \ |
| 110 | + -d @-) |
| 111 | +
|
| 112 | + echo "$API_RESPONSE" |
| 113 | +
|
| 114 | + FEEDBACK_CONTENT=$(echo "$API_RESPONSE" | jq -r '.choices[0].message.content') |
| 115 | + echo "feedback<<EOF" >> $GITHUB_OUTPUT |
| 116 | + echo "$FEEDBACK_CONTENT" >> $GITHUB_OUTPUT |
| 117 | + echo "EOF" >> $GITHUB_OUTPUT |
| 118 | + - name: Post Feedback as PR Comment ✍️ |
| 119 | + uses: actions/github-script@v7 |
| 120 | + env: |
| 121 | + FEEDBACK_BODY: ${{ steps.ai_feedback.outputs.feedback }} |
| 122 | + with: |
| 123 | + github-token: ${{ secrets.GITHUB_TOKEN }} |
| 124 | + script: | |
| 125 | + const { owner, repo } = context.repo; |
| 126 | + const targetTitle = "Feedback"; |
| 127 | + const signature = "🤖 AI Feedback"; |
| 128 | +
|
| 129 | + const { data: pullRequests } = await github.rest.pulls.list({ |
| 130 | + owner, |
| 131 | + repo, |
| 132 | + state: "open", |
| 133 | + per_page: 100 |
| 134 | + }); |
| 135 | +
|
| 136 | + const matchingPR = pullRequests.find(pr => pr.title.trim().toLowerCase() === targetTitle.toLowerCase()); |
| 137 | + if (!matchingPR) { |
| 138 | + throw new Error(`No open pull request found with title '${targetTitle}'`); |
| 139 | + } |
| 140 | +
|
| 141 | + const prNumber = matchingPR.number; |
| 142 | +
|
| 143 | + const { data: comments } = await github.rest.issues.listComments({ |
| 144 | + owner, |
| 145 | + repo, |
| 146 | + issue_number: prNumber, |
| 147 | + per_page: 100 |
| 148 | + }); |
| 149 | +
|
| 150 | + const existing = comments.find(c => |
| 151 | + c.user?.login === "github-actions[bot]" && |
| 152 | + c.body?.includes(signature) |
| 153 | + ); |
| 154 | +
|
| 155 | + const timestamp = new Date().toISOString(); |
| 156 | + const newEntry = `🕒 _Posted on ${timestamp}_\n\n${process.env.FEEDBACK_BODY}\n\n---\n`; |
| 157 | +
|
| 158 | + if (existing) { |
| 159 | + // Extract previous entries and wrap them in a collapsible block |
| 160 | + const previousContent = existing.body.replace(/^### 🤖 AI Feedback\s*/, '').trim(); |
| 161 | + const collapsed = `<details><summary>Previous Feedback</summary>\n\n${previousContent}\n</details>`; |
| 162 | +
|
| 163 | + const updatedBody = `### ${signature}\n\n${newEntry}${collapsed}`; |
| 164 | + await github.rest.issues.updateComment({ |
| 165 | + owner, |
| 166 | + repo, |
| 167 | + comment_id: existing.id, |
| 168 | + body: updatedBody |
| 169 | + }); |
| 170 | + console.log(`🔄 Updated existing comment on PR #${prNumber}`); |
| 171 | + } else { |
| 172 | + const body = `### ${signature}\n\n${newEntry}`; |
| 173 | + await github.rest.issues.createComment({ |
| 174 | + owner, |
| 175 | + repo, |
| 176 | + issue_number: prNumber, |
| 177 | + body |
| 178 | + }); |
| 179 | + console.log(`🆕 Posted new comment on PR #${prNumber}`); |
| 180 | + } |
| 181 | + run-autograding-tests: |
| 182 | + name: Autograding |
| 183 | + runs-on: ubuntu-latest |
| 184 | + if: github.actor != 'github-classroom[bot]' |
| 185 | + steps: |
| 186 | + - name: Set up Java 25 |
| 187 | + uses: actions/setup-java@v5 |
| 188 | + with: |
| 189 | + distribution: 'temurin' |
| 190 | + java-version: '25' |
| 191 | + |
| 192 | + - name: Checkout code |
| 193 | + uses: actions/checkout@v5 |
| 194 | + |
| 195 | + - name: Compilation Check |
| 196 | + id: compilation-check |
| 197 | + uses: classroom-resources/autograding-command-grader@v1 |
| 198 | + with: |
| 199 | + test-name: Compilation Check |
| 200 | + command: mvn -ntp compile |
| 201 | + timeout: 10 |
| 202 | + max-score: 1 |
| 203 | + |
| 204 | + - name: Basic Tests |
| 205 | + id: basic-tests |
| 206 | + uses: classroom-resources/autograding-command-grader@v1 |
| 207 | + with: |
| 208 | + test-name: Basic Tests |
| 209 | + command: mvn -ntp test -Dtest=BasicTest |
| 210 | + timeout: 10 |
| 211 | + max-score: 1 |
| 212 | + |
| 213 | + - name: Edge Case Tests |
| 214 | + id: edge-case-tests |
| 215 | + uses: classroom-resources/autograding-command-grader@v1 |
| 216 | + with: |
| 217 | + test-name: Edge Case Tests |
| 218 | + command: mvn -ntp test -Dtest=EdgeCaseTest |
| 219 | + timeout: 10 |
| 220 | + max-score: 1 |
| 221 | + |
| 222 | + - name: Autograding Reporter |
| 223 | + uses: classroom-resources/autograding-grading-reporter@v1 |
| 224 | + env: |
| 225 | + COMPILATION-CHECK_RESULTS: "${{steps.compilation-check.outputs.result}}" |
| 226 | + BASIC-TESTS_RESULTS: "${{steps.basic-tests.outputs.result}}" |
| 227 | + EDGE-CASE-TESTS_RESULTS: "${{steps.edge-case-tests.outputs.result}}" |
| 228 | + with: |
| 229 | + runners: compilation-check,basic-tests,edge-case-tests |
0 commit comments