diff --git a/learning-room/.github/workflows/autograder-capstone.yml b/learning-room/.github/workflows/autograder-capstone.yml index a58111c..c77e8be 100644 --- a/learning-room/.github/workflows/autograder-capstone.yml +++ b/learning-room/.github/workflows/autograder-capstone.yml @@ -14,6 +14,10 @@ permissions: contents: read pull-requests: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: validate-agent: name: Verify Agent File @@ -28,44 +32,42 @@ jobs: run: | # Find new/modified .md files in agents/ or community-agents/ AGENTS=$(find agents community-agents -name '*.md' 2>/dev/null || true) - + if [ -z "$AGENTS" ]; then echo "found=false" >> "$GITHUB_OUTPUT" exit 0 fi - + echo "found=true" >> "$GITHUB_OUTPUT" FIRST=$(echo "$AGENTS" | head -1) echo "file=$FIRST" >> "$GITHUB_OUTPUT" - CHECK_FILE="/tmp/capstone-agent.md" - sed '1s/^\xEF\xBB\xBF//' "$FIRST" > "$CHECK_FILE" - + ERRORS="" - + # Check YAML frontmatter exists (starts with ---) - if head -1 "$CHECK_FILE" | grep -q '^---'; then + if head -1 "$FIRST" | grep -q '^---'; then echo "has_frontmatter=true" >> "$GITHUB_OUTPUT" else echo "has_frontmatter=false" >> "$GITHUB_OUTPUT" ERRORS="missing YAML frontmatter" fi - + # Check for responsibilities section - if grep -qi '## responsibilities\|## what this agent does' "$CHECK_FILE"; then + if grep -qi '## responsibilities\|## what this agent does' "$FIRST"; then echo "has_responsibilities=true" >> "$GITHUB_OUTPUT" else echo "has_responsibilities=false" >> "$GITHUB_OUTPUT" ERRORS="$ERRORS${ERRORS:+, }missing responsibilities section" fi - + # Check for guardrails section - if grep -qi '## guardrails\|## limitations\|## boundaries' "$CHECK_FILE"; then + if grep -qi '## guardrails\|## limitations\|## boundaries' "$FIRST"; then echo "has_guardrails=true" >> "$GITHUB_OUTPUT" else echo "has_guardrails=false" >> "$GITHUB_OUTPUT" ERRORS="$ERRORS${ERRORS:+, }missing guardrails section" fi - + echo "errors=$ERRORS" >> "$GITHUB_OUTPUT" - name: Post result @@ -105,13 +107,70 @@ jobs: 'Excellent capstone work!' ].join('\n'); } + const MARKER = '## Challenge 16:'; try { - await github.rest.issues.createComment({ + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.issue.number, - body + issue_number: context.issue.number }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } } catch (error) { console.error('Could not post Challenge 16 result:', error.message); } + + - name: Post workflow error notice + if: failure() + uses: actions/github-script@v7 + with: + script: | + const MARKER = '## Challenge 16:'; + const body = [ + '## Challenge 16: Check could not complete', + '', + 'The automated agent file check ran into an unexpected error. Your work is not affected.', + 'Keep going and ask your facilitator, or mention @facilitator in a comment.', + '', + '---', + '*Automated message from Learning Room Bot.*' + ].join('\n'); + try { + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } + } catch (err) { + console.error('Could not post error notice:', err.message); + } diff --git a/learning-room/.github/workflows/autograder-conflicts.yml b/learning-room/.github/workflows/autograder-conflicts.yml index 1749172..bb8985f 100644 --- a/learning-room/.github/workflows/autograder-conflicts.yml +++ b/learning-room/.github/workflows/autograder-conflicts.yml @@ -11,6 +11,10 @@ permissions: contents: read pull-requests: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: check-conflict-markers: name: Verify Conflict Resolution @@ -75,13 +79,70 @@ jobs: 'No conflict markers found and the file has substantive content. Well done!' ].join('\n'); } + const MARKER = '## Challenge 7:'; try { - await github.rest.issues.createComment({ + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.issue.number, - body + issue_number: context.issue.number }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } } catch (error) { console.error('Could not post Challenge 7 result:', error.message); } + + - name: Post workflow error notice + if: failure() + uses: actions/github-script@v7 + with: + script: | + const MARKER = '## Challenge 7:'; + const body = [ + '## Challenge 7: Check could not complete', + '', + 'The automated conflict marker check ran into an unexpected error. Your work is not affected.', + 'Keep going and ask your facilitator, or mention @facilitator in a comment.', + '', + '---', + '*Automated message from Learning Room Bot.*' + ].join('\n'); + try { + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } + } catch (err) { + console.error('Could not post error notice:', err.message); + } diff --git a/learning-room/.github/workflows/autograder-local-commit.yml b/learning-room/.github/workflows/autograder-local-commit.yml index a3e7f51..cbfa0d9 100644 --- a/learning-room/.github/workflows/autograder-local-commit.yml +++ b/learning-room/.github/workflows/autograder-local-commit.yml @@ -11,6 +11,10 @@ permissions: contents: read pull-requests: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: check-local-commit: name: Verify Local Commit Exists @@ -66,13 +70,70 @@ jobs: 'Your local Git workflow is working. Well done!' ].join('\n'); } + const MARKER = '## Challenge 10:'; try { - await github.rest.issues.createComment({ + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.issue.number, - body + issue_number: context.issue.number }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } } catch (error) { console.error('Could not post Challenge 10 result:', error.message); } + + - name: Post workflow error notice + if: failure() + uses: actions/github-script@v7 + with: + script: | + const MARKER = '## Challenge 10:'; + const body = [ + '## Challenge 10: Check could not complete', + '', + 'The automated local commit check ran into an unexpected error. Your work is not affected.', + 'Keep going and ask your facilitator, or mention @facilitator in a comment.', + '', + '---', + '*Automated message from Learning Room Bot.*' + ].join('\n'); + try { + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } + } catch (err) { + console.error('Could not post error notice:', err.message); + } diff --git a/learning-room/.github/workflows/autograder-template.yml b/learning-room/.github/workflows/autograder-template.yml index 57a490c..54336b6 100644 --- a/learning-room/.github/workflows/autograder-template.yml +++ b/learning-room/.github/workflows/autograder-template.yml @@ -11,6 +11,10 @@ permissions: contents: read pull-requests: write +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + jobs: check-template: name: Verify Custom Issue Template @@ -28,28 +32,25 @@ jobs: ! -name 'challenge-*.yml' \ ! -name 'bonus-*.yml' \ ! -name 'config.yml' 2>/dev/null || true) - + if [ -z "$TEMPLATES" ]; then echo "found=false" >> "$GITHUB_OUTPUT" exit 0 fi - + echo "found=true" >> "$GITHUB_OUTPUT" - - # Check the first custom template for a name field. - # Normalize a UTF-8 BOM because Windows tooling may add one. + + # Check the first custom template for a name field FIRST=$(echo "$TEMPLATES" | head -1) echo "file=$FIRST" >> "$GITHUB_OUTPUT" - CHECK_FILE="/tmp/custom-issue-template.yml" - sed '1s/^\xEF\xBB\xBF//' "$FIRST" > "$CHECK_FILE" - - if grep -q '^name:' "$CHECK_FILE"; then + + if grep -q '^name:' "$FIRST"; then echo "has_name=true" >> "$GITHUB_OUTPUT" else echo "has_name=false" >> "$GITHUB_OUTPUT" fi - - if grep -q '^description:' "$CHECK_FILE"; then + + if grep -q '^description:' "$FIRST"; then echo "has_desc=true" >> "$GITHUB_OUTPUT" else echo "has_desc=false" >> "$GITHUB_OUTPUT" @@ -92,13 +93,70 @@ jobs: 'Found `' + file + '` with valid `name` and `description` fields. Well done!' ].join('\n'); } + const MARKER = '## Challenge 14:'; try { - await github.rest.issues.createComment({ + const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, - issue_number: context.issue.number, - body + issue_number: context.issue.number }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } } catch (error) { console.error('Could not post Challenge 14 result:', error.message); } + + - name: Post workflow error notice + if: failure() + uses: actions/github-script@v7 + with: + script: | + const MARKER = '## Challenge 14:'; + const body = [ + '## Challenge 14: Check could not complete', + '', + 'The automated issue template check ran into an unexpected error. Your work is not affected.', + 'Keep going and ask your facilitator, or mention @facilitator in a comment.', + '', + '---', + '*Automated message from Learning Room Bot.*' + ].join('\n'); + try { + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body + }); + } + } catch (err) { + console.error('Could not post error notice:', err.message); + } diff --git a/learning-room/.github/workflows/autograder-watchdog.yml b/learning-room/.github/workflows/autograder-watchdog.yml new file mode 100644 index 0000000..e5e77a0 --- /dev/null +++ b/learning-room/.github/workflows/autograder-watchdog.yml @@ -0,0 +1,99 @@ +name: Autograder Failure Watchdog +# Last-resort safety net for classroom reliability. +# Fires after any autograder workflow completes with conclusion=failure. +# If the in-job error handler already posted a comment, this is a no-op. +# If not (e.g. the runner was abandoned before the fallback step ran), +# this posts a friendly message to the student's PR so they never see silence. + +on: + workflow_run: + workflows: + - "Challenge 7: Merge Conflict Resolution Check" + - "Challenge 10: Local Commit Check" + - "Challenge 14: Issue Template Validation" + - "Challenge 16: Capstone Agent File Validation" + types: [completed] + +permissions: + pull-requests: write + +jobs: + notify-on-failure: + name: Post friendly notice if job failed silently + runs-on: ubuntu-latest + # Only act on hard failures, not cancellations (concurrency group cancels are expected). + if: github.event.workflow_run.conclusion == 'failure' + + steps: + - name: Find PR and post notice if needed + uses: actions/github-script@v7 + with: + script: | + const run = context.payload.workflow_run; + const workflowName = run.name; + const headBranch = run.head_branch; + const runCreatedAt = new Date(run.created_at); + + const { data: prs } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.repo.owner}:${headBranch}` + }); + + if (prs.length === 0) { + console.log('No open PR found for branch:', headBranch); + return; + } + + const pr = prs[0]; + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number + }); + + const challengeMarkers = [ + '## Challenge 7:', + '## Challenge 10:', + '## Challenge 14:', + '## Challenge 16:' + ]; + + const alreadyPosted = comments.some(c => + c.user.type === 'Bot' && + new Date(c.updated_at) > runCreatedAt && + challengeMarkers.some(m => c.body.includes(m)) + ); + + if (alreadyPosted) { + console.log('Result already posted by in-job handler -- watchdog is a no-op.'); + return; + } + + const m = workflowName.match(/Challenge (\d+)/); + const num = m ? m[1] : '?'; + + const body = [ + `## Challenge ${num}: Check could not complete`, + '', + `The automated check ran into an unexpected problem on our end. Your work is not affected.`, + '', + 'Keep going and let your facilitator know, or leave a comment mentioning @facilitator.', + '', + '---', + '*Automated message from Learning Room Bot.*' + ].join('\n'); + + try { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body + }); + console.log(`Watchdog posted notice on PR #${pr.number}`); + } catch (err) { + console.error('Watchdog could not post notice:', err.message); + } \ No newline at end of file