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