Practice Issue to use for 4.2 #127
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Skills Progression Tracker | |
| # Tracks student progress, unlocks challenges, awards badges | |
| # Runs when PRs are merged to celebrate achievements and motivate learning | |
| on: | |
| pull_request: | |
| types: [closed] | |
| issues: | |
| types: [closed] | |
| workflow_dispatch: | |
| inputs: | |
| student_username: | |
| description: 'Student GitHub username' | |
| required: false | |
| action: | |
| description: 'Action (check_progress, assign_next, reset)' | |
| required: false | |
| default: 'check_progress' | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| track-completion: | |
| name: Award Badge & Track Progress | |
| runs-on: ubuntu-latest | |
| if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Award achievement badge | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| try { | |
| if (!context.payload.pull_request) { | |
| console.log('Skipping achievement on non-PR event'); | |
| return; | |
| } | |
| const student = context.payload.pull_request.user.login; | |
| const prNumber = context.payload.pull_request.number; | |
| const prBody = context.payload.pull_request.body || ''; | |
| // Extract linked issue | |
| const issueMatch = prBody.match(/(?:close|closes|fix|fixes|resolve|resolves)\s+#(\d+)/i); | |
| if (!issueMatch) { | |
| console.log('No linked issue found in PR'); | |
| return; | |
| } | |
| const issueNumber = parseInt(issueMatch[1]); | |
| // Fetch issue to determine challenge type | |
| const { data: issue } = await github.rest.issues.get({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber | |
| }); | |
| // Determine skill from labels | |
| const labels = issue.labels.map(l => typeof l === 'string' ? l : l.name); | |
| let skillType = 'general-contribution'; | |
| let badgeName = 'Contributor'; | |
| let skillDescription = 'You made a meaningful contribution!'; | |
| if (labels.includes('skill: markdown')) { | |
| skillType = 'markdown-master'; | |
| badgeName = '📝 Markdown Master'; | |
| skillDescription = 'You mastered proper markdown syntax and document structure!'; | |
| } else if (labels.includes('skill: accessibility')) { | |
| skillType = 'accessibility-advocate'; | |
| badgeName = '♿ Accessibility Advocate'; | |
| skillDescription = 'You made content more accessible for everyone!'; | |
| } else if (labels.includes('skill: review')) { | |
| skillType = 'code-reviewer'; | |
| badgeName = '👀 Code Reviewer'; | |
| skillDescription = 'You provided constructive, helpful feedback!'; | |
| } else if (labels.includes('skill: collaboration')) { | |
| skillType = 'team-player'; | |
| badgeName = '🤝 Team Player'; | |
| skillDescription = 'You collaborated effectively with your peers!'; | |
| } | |
| // Build achievement comment | |
| const achievementBody = [ | |
| '## Achievement Unlocked! 🏆', | |
| '', | |
| `**${badgeName}** — ${skillDescription}`, | |
| '', | |
| '### How You Got This', | |
| '', | |
| `Merged PR #${prNumber} (closes #${issueNumber})`, | |
| '', | |
| `Challenge: ${issue.title}`, | |
| '', | |
| '### What\'s Next?', | |
| '', | |
| '✅ Check your [Learning Path](../docs/LEARNING_PATHS.md) for the next challenge', | |
| '📖 Review available [Challenges](docs/CHALLENGES.md)', | |
| '💬 Comment if you want to claim the next issue', | |
| '', | |
| '**Keep going! You\'re building real open source experience.** 🚀', | |
| '', | |
| '---', | |
| '*Progress tracked by the Learning Room Skills Engine*' | |
| ].join('\n'); | |
| // Post achievement comment on the merged PR | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: achievementBody | |
| }); | |
| // Close the linked issue with a celebration comment | |
| const issueClosingComment = [ | |
| `## Issue Resolved ✅`, | |
| '', | |
| `Great work @${student}! Your PR #${prNumber} has been merged.`, | |
| '', | |
| 'Next steps:', | |
| '1. Celebrate your achievement! 🎉', | |
| '2. Check the [Learning Paths](../docs/LEARNING_PATHS.md) for your next challenge', | |
| '3. Claim the next issue with a comment' | |
| ].join('\n'); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issueNumber, | |
| body: issueClosingComment | |
| }); | |
| // Add achievement label to PR | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: [`achievement: ${skillType}`] | |
| }); | |
| } catch (labelError) { | |
| console.log('Could not add label:', labelError.message); | |
| } | |
| } catch (error) { | |
| console.error('Error awarding achievement:', error); | |
| } | |
| unlock-next-challenge: | |
| name: Check for Next Challenge | |
| runs-on: ubuntu-latest | |
| if: github.event.pull_request.merged == true | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Find next available challenge | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| try { | |
| const student = context.payload.pull_request.user.login; | |
| // Get all open issues unassigned or not yet claimed by this student | |
| const { data: openChallenges } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'challenge' | |
| }); | |
| if (openChallenges.length === 0) { | |
| console.log('No more challenges available'); | |
| return; | |
| } | |
| // Find first challenge not assigned to this student | |
| const nextChallenge = openChallenges.find(issue => { | |
| const assignees = (issue.assignees || []).map(a => a.login); | |
| return !assignees.includes(student); | |
| }); | |
| if (nextChallenge) { | |
| const suggestionComment = [ | |
| `Hi @${student}! 👋`, | |
| '', | |
| 'Congratulations on completing your last challenge! **You\'re on a roll!** 🎉', | |
| '', | |
| `### Next Challenge Available:`, | |
| `[${nextChallenge.title}](${nextChallenge.html_url})`, | |
| '', | |
| 'Would you like to claim it? Comment below with:', | |
| '> I\'d like to work on this challenge!', | |
| '', | |
| 'Questions? Ask in the issue comments - that\'s what they\'re for!', | |
| '', | |
| '---', | |
| '*Learning Room Skills Engine*' | |
| ].join('\n'); | |
| // Post suggestion as reply in the PR | |
| // (This helps students see the next challenge right away) | |
| console.log('Next challenge found:', nextChallenge.number); | |
| } | |
| } catch (error) { | |
| console.error('Error finding next challenge:', error); | |
| } |