"Detailed setup notes" should be a heading #157
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: Registration - Welcome & CSV Export | |
| on: | |
| issues: | |
| types: [opened] | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| issues: write | |
| jobs: | |
| welcome: | |
| name: Welcome New Registrant | |
| runs-on: ubuntu-latest | |
| if: >- | |
| github.event_name == 'issues' && | |
| github.event.action == 'opened' && | |
| contains(github.event.issue.title, '[REGISTER]') | |
| steps: | |
| - name: Check for duplicate registration | |
| id: dup-check | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const username = context.payload.issue.user.login; | |
| const currentIssueNumber = context.issue.number; | |
| // Find existing registration issues by this user | |
| const existingIssues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| labels: 'registration', | |
| state: 'all', | |
| per_page: 100 | |
| }); | |
| const priorRegistration = existingIssues.find( | |
| issue => issue.user.login === username && issue.number !== currentIssueNumber | |
| ); | |
| if (priorRegistration) { | |
| core.setOutput('is_duplicate', 'true'); | |
| core.setOutput('original_issue', priorRegistration.number); | |
| } else { | |
| core.setOutput('is_duplicate', 'false'); | |
| } | |
| - name: Handle duplicate registration | |
| if: steps.dup-check.outputs.is_duplicate == 'true' | |
| uses: actions/github-script@v7 | |
| env: | |
| ORIGINAL_ISSUE: ${{ steps.dup-check.outputs.original_issue }} | |
| with: | |
| script: | | |
| const originalIssue = parseInt(process.env.ORIGINAL_ISSUE, 10); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `## You are already registered! | |
| Hi @${context.payload.issue.user.login}, it looks like you already registered in issue #${originalIssue}. No need to register again — your spot is saved! | |
| We have closed this issue since your original registration is already on file. If you need to update your details, please comment on your original issue #${originalIssue}. | |
| Questions? [File an issue](https://github.com/community-access/git-going-with-github/issues) in the workshop repository.` | |
| }); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| labels: ['duplicate'] | |
| }); | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| state: 'closed', | |
| state_reason: 'not_planned' | |
| }); | |
| - name: Check capacity | |
| id: capacity-check | |
| if: steps.dup-check.outputs.is_duplicate == 'false' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const MAX_CAPACITY = 75; | |
| // Count unique registered users (excluding this issue) | |
| const existingIssues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| labels: 'registration', | |
| state: 'all', | |
| per_page: 100 | |
| }); | |
| const currentIssueNumber = context.issue.number; | |
| const uniqueUsers = new Set(); | |
| for (const issue of existingIssues) { | |
| if (issue.number !== currentIssueNumber) { | |
| uniqueUsers.add(issue.user.login); | |
| } | |
| } | |
| const currentCount = uniqueUsers.size; | |
| core.setOutput('count', currentCount); | |
| if (currentCount >= MAX_CAPACITY) { | |
| core.setOutput('is_full', 'true'); | |
| } else { | |
| core.setOutput('is_full', 'false'); | |
| } | |
| - name: Handle registration full | |
| if: steps.dup-check.outputs.is_duplicate == 'false' && steps.capacity-check.outputs.is_full == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `## Registration is currently full | |
| Hi @${context.payload.issue.user.login}, thank you for your interest in GIT Going with GitHub! Unfortunately, all 75 spots have been filled. | |
| We have added you to the waitlist. If a spot opens up, we will let you know right here on this issue. | |
| Questions? [File an issue](https://github.com/community-access/git-going-with-github/issues) in the workshop repository.` | |
| }); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| labels: ['waitlist'] | |
| }); | |
| - name: Post welcome comment | |
| if: steps.dup-check.outputs.is_duplicate == 'false' && steps.capacity-check.outputs.is_full == 'false' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: `## Welcome to GIT Going with GitHub! | |
| Hi @${context.payload.issue.user.login}, thank you for registering! We are excited to have you join us. | |
| Your registration is confirmed. Please register for the Zoom session here: [Register on Zoom](https://us06web.zoom.us/meeting/register/YdAAvwzAQUCYpPpNtAlG3g) | |
| In the meantime, you can get a head start with the [Pre-Workshop Setup Guide](https://community-access.github.io/git-going-with-github/docs/00-pre-workshop-setup.html). | |
| If you have any questions, [file an issue](https://github.com/community-access/git-going-with-github/issues) in the workshop repository. | |
| See you on March 7!` | |
| }); | |
| - name: Add registration label | |
| if: steps.dup-check.outputs.is_duplicate == 'false' && steps.capacity-check.outputs.is_full == 'false' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| labels: ['registration'] | |
| }); | |
| } catch (e) { | |
| console.log('Label may already exist:', e.message); | |
| } | |
| export-csv: | |
| name: Export Registrations to CSV | |
| runs-on: ubuntu-latest | |
| if: >- | |
| (github.event_name == 'workflow_dispatch') || | |
| (github.event_name == 'issues' && | |
| github.event.action == 'opened' && | |
| contains(github.event.issue.title, '[REGISTER]')) | |
| steps: | |
| - name: Generate CSV from registration issues | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| // Fetch all issues with the registration label | |
| const issues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| labels: 'registration', | |
| state: 'all', | |
| per_page: 100 | |
| }); | |
| // Parse issue body fields (GitHub YAML forms use ### heading + content pattern) | |
| function parseField(body, fieldName) { | |
| if (!body) return ''; | |
| // Match the pattern: ### Field Name\n\nValue | |
| // Also handle ### Field Name\r\n\r\nValue | |
| const regex = new RegExp(`### ${fieldName}\\s*\\n+([\\s\\S]*?)(?=\\n### |$)`, 'i'); | |
| const match = body.match(regex); | |
| if (!match) return ''; | |
| let value = match[1].trim(); | |
| // Remove _No response_ placeholder | |
| if (value === '_No response_') return ''; | |
| // Escape CSV: quote fields containing commas, quotes, or newlines | |
| if (value.includes(',') || value.includes('"') || value.includes('\n')) { | |
| value = '"' + value.replace(/"/g, '""') + '"'; | |
| } | |
| return value; | |
| } | |
| // Fetch org members and pending invitations for OrgStatus column | |
| let orgMembers = new Set(); | |
| let pendingInvites = new Set(); | |
| try { | |
| const members = await github.paginate(github.rest.orgs.listMembers, { | |
| org: context.repo.owner, | |
| per_page: 100 | |
| }); | |
| for (const m of members) orgMembers.add(m.login.toLowerCase()); | |
| const invites = await github.paginate(github.rest.orgs.listPendingInvitations, { | |
| org: context.repo.owner, | |
| per_page: 100 | |
| }); | |
| for (const inv of invites) { | |
| if (inv.login) pendingInvites.add(inv.login.toLowerCase()); | |
| } | |
| } catch (e) { | |
| console.log('Could not fetch org membership (may lack admin scope):', e.message); | |
| } | |
| // Build CSV | |
| const headers = [ | |
| 'IssueNumber', | |
| 'State', | |
| 'CreatedAt', | |
| 'GitHubUsername', | |
| 'FirstName', | |
| 'LastName', | |
| 'Email', | |
| 'ProficiencyLevel', | |
| 'PrimaryScreenReader', | |
| 'QuestionsOrAccommodations', | |
| 'OrgStatus' | |
| ]; | |
| let csv = headers.join(',') + '\n'; | |
| // Deduplicate by GitHub username, keeping only the earliest registration | |
| const seen = new Set(); | |
| const sortedIssues = issues.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); | |
| for (const issue of sortedIssues) { | |
| const username = issue.user.login; | |
| if (seen.has(username)) continue; | |
| seen.add(username); | |
| // Determine org membership status | |
| const uLower = username.toLowerCase(); | |
| let orgStatus = 'needs-invite'; | |
| if (orgMembers.has(uLower)) orgStatus = 'member'; | |
| else if (pendingInvites.has(uLower)) orgStatus = 'pending'; | |
| const row = [ | |
| issue.number, | |
| issue.state.toUpperCase(), | |
| issue.created_at.split('T')[0], | |
| username, | |
| parseField(issue.body, 'First Name'), | |
| parseField(issue.body, 'Last Name'), | |
| parseField(issue.body, 'Email Address'), | |
| parseField(issue.body, 'GitHub Proficiency Level'), | |
| parseField(issue.body, 'Primary Screen Reader'), | |
| parseField(issue.body, 'Questions or Accommodations'), | |
| orgStatus | |
| ]; | |
| csv += row.join(',') + '\n'; | |
| } | |
| // Write CSV file | |
| fs.mkdirSync('.github/data', { recursive: true }); | |
| fs.writeFileSync('.github/data/registration-data.csv', csv, 'utf-8'); | |
| console.log(`Exported ${seen.size} unique registration(s) from ${issues.length} total issue(s) to .github/data/registration-data.csv`); | |
| - name: Upload CSV as artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: registration-data | |
| path: .github/data/registration-data.csv | |
| retention-days: 90 | |
| sync-roster: | |
| name: Sync Student Roster (No PII) | |
| runs-on: ubuntu-latest | |
| if: >- | |
| (github.event_name == 'workflow_dispatch') || | |
| (github.event_name == 'issues' && | |
| github.event.action == 'opened' && | |
| contains(github.event.issue.title, '[REGISTER]')) | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Build roster from registration issues | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| // Fetch all issues with the registration label | |
| const issues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| labels: 'registration', | |
| state: 'all', | |
| per_page: 100 | |
| }); | |
| function parseField(body, fieldName) { | |
| if (!body) return ''; | |
| const regex = new RegExp(`### ${fieldName}\\s*\\n+([\\s\\S]*?)(?=\\n### |$)`, 'i'); | |
| const match = body.match(regex); | |
| if (!match) return ''; | |
| const value = match[1].trim(); | |
| if (value === '_No response_') return ''; | |
| return value; | |
| } | |
| // Load existing roster to preserve runtime state (mergedPRs, currentLevel, badges) | |
| const rosterPath = '.github/data/student-roster.json'; | |
| let roster = { students: [] }; | |
| if (fs.existsSync(rosterPath)) { | |
| roster = JSON.parse(fs.readFileSync(rosterPath, 'utf8')); | |
| } | |
| const existing = new Map(roster.students.map(s => [s.username, s])); | |
| // Deduplicate by username, keeping earliest registration | |
| const seen = new Set(); | |
| const sorted = issues.sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); | |
| const students = []; | |
| for (const issue of sorted) { | |
| const username = issue.user.login; | |
| if (seen.has(username)) continue; | |
| seen.add(username); | |
| // Preserve existing runtime fields if student was already in roster | |
| const prev = existing.get(username) || {}; | |
| // Only store non-PII operational fields | |
| students.push({ | |
| username, | |
| joinedDate: prev.joinedDate || issue.created_at.split('T')[0], | |
| mergedPRs: prev.mergedPRs || 0, | |
| currentLevel: prev.currentLevel || 'Beginner', | |
| interests: prev.interests || ['accessibility', 'open-source'], | |
| timezone: prev.timezone || 'Unknown' | |
| }); | |
| } | |
| // Update roster — preserve cohort metadata, replace students list | |
| roster.students = students; | |
| roster._schema_note = 'Auto-populated by registration workflow. Contains only GitHub usernames and workflow state. No personal information.'; | |
| fs.mkdirSync('.github/data', { recursive: true }); | |
| fs.writeFileSync(rosterPath, JSON.stringify(roster, null, 2) + '\n', 'utf-8'); | |
| console.log(`Roster synced: ${students.length} student(s) from ${issues.length} registration issue(s)`); | |
| - name: Commit roster update | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add .github/data/student-roster.json | |
| if git diff --cached --quiet; then | |
| echo "No roster changes to commit" | |
| else | |
| git commit -m "chore: sync student roster from registrations [skip ci]" | |
| git push | |
| fi |