From 5910fce0a283f06d945e92e0831eaa6ceb42ca3b Mon Sep 17 00:00:00 2001 From: GitHub Workshop Bot Date: Tue, 12 May 2026 12:35:37 -0700 Subject: [PATCH] refactor(registration): remove Classroom org-invite API automation Replaces the CLASSROOM_ORG_ADMIN_TOKEN + Classroom org-invite API path with a simpler 'post the assignment URLs only' flow. The registration workflow remains responsible for capacity checks, duplicate detection, waitlist handling, label management, and CSV export -- it just stops trying to drive the GitHub org membership API. What changes - .github/workflows/registration.yml: drop the org-invite job, keep the welcome comment with Day 1 and Day 2 assignment URLs - .github/workflows/day2-release.yml: drop org-API dependency - .github/workflows/instructor-dashboard-sync.yml: drop org-API dependency and simplify the dashboard build - .github/scripts/__tests__/registration-workflow-readiness.test.js: update the assertion to match the new flow - scripts/classroom/Initialize-WorkshopSetup.ps1: remove the CLASSROOM_ORG_ADMIN_TOKEN bootstrap - admin/REGISTRATION-QUICKSTART.md, QUICK_START_SETUP.md, FINE_GRAINED_TOKEN_SETUP.md, CLASSROOM_INTEGRATION_GUIDE.md, ENROLLMENT_SETUP_CHECKLIST.md, GITHUB_CLASSROOM_ARCHITECTURE.md, IMPLEMENTATION_SUMMARY.md: drop org-admin token setup sections, document the new URL-only flow, and remove now-irrelevant architecture diagrams - admin/qa-bundle/.github/workflows/registration.yml: sync the qa-bundle copy of the new registration workflow Why Maintaining a fine-grained admin token with org invitation scope added a fragile setup step, an extra secret to rotate, and a class of failures (token expiry, scope drift, org rename) that broke registration for every cohort. Posting two assignment URLs in a welcome comment is equivalent for the student, requires zero secrets, and cannot break in production. Rollback If the new flow needs to be disabled, clear repository variables CLASSROOM_DAY1_ASSIGNMENT_URL and CLASSROOM_DAY2_ASSIGNMENT_URL. The rest of the registration workflow (capacity, duplicates, waitlist, CSV) continues running unchanged. --- .../registration-workflow-readiness.test.js | 3 +- .github/workflows/day2-release.yml | 63 +--- .../workflows/instructor-dashboard-sync.yml | 176 +++------- .github/workflows/registration.yml | 139 +------- admin/CLASSROOM_INTEGRATION_GUIDE.md | 35 +- admin/ENROLLMENT_SETUP_CHECKLIST.md | 40 +-- admin/FINE_GRAINED_TOKEN_SETUP.md | 209 ++--------- admin/GITHUB_CLASSROOM_ARCHITECTURE.md | 328 +++--------------- admin/IMPLEMENTATION_SUMMARY.md | 291 ++++------------ admin/QUICK_START_SETUP.md | 141 ++------ admin/REGISTRATION-QUICKSTART.md | 35 +- .../.github/workflows/registration.yml | 65 +--- .../classroom/Initialize-WorkshopSetup.ps1 | 31 +- 13 files changed, 283 insertions(+), 1273 deletions(-) diff --git a/.github/scripts/__tests__/registration-workflow-readiness.test.js b/.github/scripts/__tests__/registration-workflow-readiness.test.js index 2b097be..854ed14 100644 --- a/.github/scripts/__tests__/registration-workflow-readiness.test.js +++ b/.github/scripts/__tests__/registration-workflow-readiness.test.js @@ -17,10 +17,9 @@ test('registration workflow contains required label and duplicate/waitlist flows "labels: ['duplicate']", "labels: ['waitlist']", "labels: ['registration']", - 'CLASSROOM_ORG_ADMIN_TOKEN', 'CLASSROOM_DAY1_ASSIGNMENT_URL', 'CLASSROOM_DAY2_ASSIGNMENT_URL', - 'createInvitation', + 'Please reply `ack` on this issue after you confirm your Day 1 link works.', 'Upload CSV as artifact', 'Sync Student Roster (No PII)', ]; diff --git a/.github/workflows/day2-release.yml b/.github/workflows/day2-release.yml index f51685d..91121ec 100644 --- a/.github/workflows/day2-release.yml +++ b/.github/workflows/day2-release.yml @@ -13,29 +13,21 @@ jobs: release-day2-links: name: Release Day 2 Links After Day 1 Milestones runs-on: ubuntu-latest - if: vars.CLASSROOM_ORG != '' && vars.CLASSROOM_DAY2_ASSIGNMENT_URL != '' + if: vars.CLASSROOM_DAY2_ASSIGNMENT_URL != '' steps: - - name: Evaluate Day 1 completion and release Day 2 link + - name: Evaluate Day 1 completion signals and release Day 2 link uses: actions/github-script@v7 env: - CLASSROOM_ORG: ${{ vars.CLASSROOM_ORG }} DAY2_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY2_ASSIGNMENT_URL }} - CLASSROOM_TOKEN: ${{ secrets.CLASSROOM_ORG_ADMIN_TOKEN }} with: script: | - const { getOctokit } = require('@actions/github'); - - const org = (process.env.CLASSROOM_ORG || '').trim(); const day2Url = (process.env.DAY2_ASSIGNMENT_URL || '').trim(); - const classroomToken = (process.env.CLASSROOM_TOKEN || '').trim(); - if (!org || !day2Url || !classroomToken) { + if (!day2Url) { core.info('Missing required configuration for Day 2 release gate.'); return; } - const classroom = getOctokit(classroomToken); - const enrollmentIssues = await github.paginate(github.rest.issues.listForRepo, { owner: context.repo.owner, repo: context.repo.repo, @@ -44,19 +36,6 @@ jobs: per_page: 100 }); - const repos = await classroom.paginate(classroom.rest.repos.listForOrg, { - org, - type: 'private', - per_page: 100 - }); - - function findStudentRepo(username) { - const normalized = username.toLowerCase(); - const exact = repos.find(r => r.name.toLowerCase() === `learning-room-${normalized}`); - if (exact) return exact; - return repos.find(r => r.name.toLowerCase().includes(normalized)); - } - for (const enrollment of enrollmentIssues) { const alreadyReleased = enrollment.labels.some(l => l.name === 'day2-released'); if (alreadyReleased) { @@ -64,30 +43,7 @@ jobs: } const student = enrollment.user.login; - const studentRepo = findStudentRepo(student); - if (!studentRepo) { - core.info(`No student repo found for @${student} in ${org}.`); - continue; - } - - let day1Complete = false; - try { - const closedIssues = await classroom.paginate(classroom.rest.issues.listForRepo, { - owner: org, - repo: studentRepo.name, - state: 'closed', - per_page: 100 - }); - - day1Complete = closedIssues.some(issue => !issue.pull_request && /^Challenge\s+9\b/i.test(issue.title || '')); - } catch (error) { - core.warning(`Failed to inspect ${org}/${studentRepo.name}: ${error.message}`); - continue; - } - - if (!day1Complete) { - continue; - } + const hasEligibilityLabel = enrollment.labels.some(l => l.name === 'day2-eligible' || l.name === 'day1-complete'); const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, @@ -96,13 +52,22 @@ jobs: per_page: 100 }); + const hasCompletionComment = comments.some(c => + (c.user?.login || '').toLowerCase() === student.toLowerCase() && + /\bday\s*1\s*complete\b|\bday1-complete\b/i.test(c.body || '') + ); + + if (!hasEligibilityLabel && !hasCompletionComment) { + continue; + } + const hasReleaseComment = comments.some(c => (c.body || '').includes('')); if (!hasReleaseComment) { const body = [ '', '## Day 2 is unlocked', '', - `Hi @${student}, you completed the Day 1 milestone.`, + `Hi @${student}, your Day 1 completion was confirmed.`, '', `- Day 2 assignment: ${day2Url}`, '', diff --git a/.github/workflows/instructor-dashboard-sync.yml b/.github/workflows/instructor-dashboard-sync.yml index 441857c..670489e 100644 --- a/.github/workflows/instructor-dashboard-sync.yml +++ b/.github/workflows/instructor-dashboard-sync.yml @@ -12,12 +12,11 @@ jobs: sync-dashboard: name: Sync Instructor Dashboard Issues runs-on: ubuntu-latest - if: vars.CLASSROOM_ORG != '' && vars.PRIVATE_STUDENT_DATA_REPO != '' + if: vars.PRIVATE_STUDENT_DATA_REPO != '' steps: - name: Aggregate student progress and sync private dashboard issues uses: actions/github-script@v7 env: - CLASSROOM_ORG: ${{ vars.CLASSROOM_ORG }} ADMIN_REPO: ${{ vars.PRIVATE_STUDENT_DATA_REPO }} STUCK_THRESHOLD_MINUTES: ${{ vars.DASHBOARD_STUCK_THRESHOLD_MINUTES }} DASHBOARD_TOKEN: ${{ secrets.INSTRUCTOR_DASHBOARD_TOKEN }} @@ -25,12 +24,11 @@ jobs: script: | const { getOctokit } = require('@actions/github'); - const org = (process.env.CLASSROOM_ORG || '').trim(); const adminRepoPath = (process.env.ADMIN_REPO || '').trim(); const dashboardToken = (process.env.DASHBOARD_TOKEN || '').trim(); const threshold = Number(process.env.STUCK_THRESHOLD_MINUTES || '20'); - if (!org || !adminRepoPath || !dashboardToken) { + if (!adminRepoPath || !dashboardToken) { core.info('Missing required configuration for instructor dashboard sync.'); return; } @@ -51,21 +49,6 @@ jobs: per_page: 100 }); - const classroomRepos = await octokit.paginate(octokit.rest.repos.listForOrg, { - org, - type: 'private', - per_page: 100 - }); - - const now = Date.now(); - - function findStudentRepo(username) { - const normalized = username.toLowerCase(); - const exact = classroomRepos.find(r => r.name.toLowerCase() === `learning-room-${normalized}`); - if (exact) return exact; - return classroomRepos.find(r => r.name.toLowerCase().includes(normalized)); - } - async function upsertDashboardIssue(student, payload) { const title = `[DASHBOARD] @${student}`; const marker = ``; @@ -74,11 +57,10 @@ jobs: `## Student: @${student}`, '', `- Status: ${payload.status}`, - `- Repo: ${payload.repoName || 'not-found'}`, - `- Current challenge: ${payload.currentChallenge}`, - `- Day 1 complete: ${payload.day1Complete ? 'yes' : 'no'}`, - `- Needs review: ${payload.needsReview ? 'yes' : 'no'}`, - `- Stuck (>= ${threshold} min): ${payload.stuck ? 'yes' : 'no'}`, + `- Day 1 acknowledged: ${payload.acknowledged ? 'yes' : 'no'}`, + `- Day 1 complete signal: ${payload.day1Complete ? 'yes' : 'no'}`, + `- Day 2 released: ${payload.day2Released ? 'yes' : 'no'}`, + `- Needs facilitator action: ${payload.needsAction ? 'yes' : 'no'}`, `- Last activity: ${payload.lastActivity || 'unknown'}`, '', `- Snapshot time: ${new Date().toISOString()}` @@ -90,9 +72,9 @@ jobs: }); const labels = ['dashboard-student', `dashboard-status-${payload.status}`]; - if (payload.needsReview) labels.push('dashboard-needs-review'); - if (payload.stuck) labels.push('dashboard-stuck'); + if (payload.needsAction) labels.push('dashboard-needs-action'); if (payload.day1Complete) labels.push('dashboard-day1-complete'); + if (payload.day2Released) labels.push('dashboard-day2-released'); if (search.data.total_count > 0) { const issueNumber = search.data.items[0].number; @@ -137,121 +119,75 @@ jobs: for (const enrollment of enrollmentIssues) { const student = enrollment.user.login; - const repo = findStudentRepo(student); - - if (!repo) { - await upsertDashboardIssue(student, { - status: 'repo-missing', - repoName: '', - currentChallenge: 'not-started', - day1Complete: false, - needsReview: false, - stuck: false, - lastActivity: '' - }); - continue; - } - - let lastActivity = repo.pushed_at || ''; - let currentChallenge = 'unknown'; + let lastActivity = enrollment.updated_at || ''; let day1Complete = false; - let needsReview = false; - let stuck = false; - let status = 'active'; + let acknowledged = false; + let day2Released = enrollment.labels.some(l => l.name === 'day2-released'); + let needsAction = false; + let status = 'awaiting-ack'; try { - const [openPrs, openIssues, closedIssues] = await Promise.all([ - octokit.paginate(octokit.rest.pulls.list, { - owner: org, - repo: repo.name, - state: 'open', - per_page: 100 - }), - octokit.paginate(octokit.rest.issues.listForRepo, { - owner: org, - repo: repo.name, - state: 'open', - per_page: 100 - }), - octokit.paginate(octokit.rest.issues.listForRepo, { - owner: org, - repo: repo.name, - state: 'closed', - per_page: 100 - }) - ]); + const comments = await octokit.paginate(octokit.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: enrollment.number, + per_page: 100 + }); - const challengeIssuesOpen = openIssues - .filter(i => !i.pull_request && /^Challenge\s+\d+/i.test(i.title || '')); + const sortedComments = comments + .map(c => ({ body: c.body || '', user: c.user?.login || '', created_at: c.created_at || '' })) + .sort((a, b) => new Date(a.created_at) - new Date(b.created_at)); - const challengeIssuesClosed = closedIssues - .filter(i => !i.pull_request && /^Challenge\s+\d+/i.test(i.title || '')); + acknowledged = sortedComments.some(c => + c.user.toLowerCase() === student.toLowerCase() && /\back\b/i.test(c.body) + ); - const openMatch = challengeIssuesOpen - .map(i => ({ n: Number((i.title.match(/Challenge\s+(\d+)/i) || [])[1] || 0), updated_at: i.updated_at })) - .filter(i => i.n > 0) - .sort((a, b) => a.n - b.n); + day1Complete = + enrollment.labels.some(l => l.name === 'day1-complete' || l.name === 'day2-eligible') || + sortedComments.some(c => + c.user.toLowerCase() === student.toLowerCase() && + /\bday\s*1\s*complete\b|\bday1-complete\b/i.test(c.body) + ); - const closedMatch = challengeIssuesClosed - .map(i => Number((i.title.match(/Challenge\s+(\d+)/i) || [])[1] || 0)) - .filter(n => n > 0) - .sort((a, b) => b - a); + day2Released = day2Released || enrollment.labels.some(l => l.name === 'day2-released'); - if (openMatch.length > 0) { - currentChallenge = String(openMatch[0].n); - const latestOpenUpdated = new Date(openMatch[openMatch.length - 1].updated_at).getTime(); - const minutesSinceOpenUpdate = (now - latestOpenUpdated) / 60000; - if (minutesSinceOpenUpdate >= threshold) { - stuck = true; - } - } else if (closedMatch.length > 0) { - currentChallenge = `next:${closedMatch[0] + 1}`; + if (enrollment.labels.some(l => l.name === 'needs-info')) { + status = 'needs-info'; + needsAction = true; + } else if (day2Released) { + status = 'day2-released'; + } else if (day1Complete) { + status = 'day1-complete'; + } else if (acknowledged) { + status = 'active-day1'; } else { - currentChallenge = '1'; + status = 'awaiting-ack'; + needsAction = true; } - day1Complete = challengeIssuesClosed.some(i => /^Challenge\s+9\b/i.test(i.title || '')); - - if (openPrs.length > 0) { - const oldestPrMinutes = openPrs - .map(pr => (now - new Date(pr.created_at).getTime()) / 60000) - .sort((a, b) => b - a)[0]; - if (oldestPrMinutes >= threshold) { - needsReview = true; + const lastComment = sortedComments[sortedComments.length - 1]; + if (lastComment?.created_at) { + const last = new Date(lastComment.created_at).getTime(); + if (!Number.isNaN(last)) { + const mins = (Date.now() - last) / 60000; + if (status === 'active-day1' && mins >= threshold) { + needsAction = true; + } + lastActivity = new Date(last).toISOString(); } } - - const timestamps = [ - repo.pushed_at, - ...openPrs.map(pr => pr.updated_at), - ...challengeIssuesOpen.map(i => i.updated_at), - ...challengeIssuesClosed.slice(0, 3).map(i => i.updated_at) - ].filter(Boolean).map(ts => new Date(ts).getTime()); - - if (timestamps.length > 0) { - const latest = Math.max(...timestamps); - lastActivity = new Date(latest).toISOString(); - } - - if (stuck) { - status = 'stuck'; - } else if (day1Complete) { - status = 'day1-complete'; - } else { - status = 'active'; - } } catch (error) { core.warning(`Failed to build snapshot for @${student}: ${error.message}`); status = 'error'; + needsAction = true; } await upsertDashboardIssue(student, { status, - repoName: `${org}/${repo.name}`, - currentChallenge, + acknowledged, day1Complete, - needsReview, - stuck, + day2Released, + needsAction, lastActivity }); } diff --git a/.github/workflows/registration.yml b/.github/workflows/registration.yml index 938e772..1ca8cdd 100644 --- a/.github/workflows/registration.yml +++ b/.github/workflows/registration.yml @@ -51,13 +51,10 @@ jobs: env: ORIGINAL_ISSUE: ${{ steps.dup-check.outputs.original_issue }} DAY1_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY1_ASSIGNMENT_URL }} - CLASSROOM_JOIN_URL: ${{ vars.CLASSROOM_JOIN_URL }} with: script: | const links = []; const day1 = (process.env.DAY1_ASSIGNMENT_URL || '').trim(); - const join = (process.env.CLASSROOM_JOIN_URL || '').trim(); - if (join) links.push(`- Classroom join link: ${join}`); if (day1) links.push(`- Day 1 assignment link: ${day1}`); const quickLinksSection = links.length @@ -336,55 +333,6 @@ jobs: ].join('\n') }); - - name: Invite learner to classroom organization (optional) - id: classroom-invite - if: >- - steps.dup-check.outputs.is_duplicate == 'false' && - steps.storage-guardrail.outputs.configured == 'true' && - steps.parse.outputs.is_valid == 'true' && - vars.CLASSROOM_ORG != '' - uses: actions/github-script@v7 - env: - CLASSROOM_ORG: ${{ vars.CLASSROOM_ORG }} - with: - github-token: ${{ secrets.CLASSROOM_ORG_ADMIN_TOKEN }} - script: | - const org = process.env.CLASSROOM_ORG; - const username = context.payload.issue.user.login; - const userId = context.payload.issue.user.id; - let status = 'skipped'; - - try { - await github.rest.orgs.checkMembershipForUser({ org, username }); - status = 'already-member'; - } catch (error) { - if (error.status !== 404) { - throw error; - } - - const invites = await github.paginate(github.rest.orgs.listPendingInvitations, { - org, - per_page: 100 - }); - - const hasPendingInvite = invites.some(invite => - (invite.login || '').toLowerCase() === username.toLowerCase() - ); - - if (hasPendingInvite) { - status = 'already-invited'; - } else { - await github.rest.orgs.createInvitation({ - org, - invitee_id: userId, - role: 'direct_member' - }); - status = 'invited'; - } - } - - core.setOutput('status', status); - - name: Post beginner-friendly success comment if: >- steps.dup-check.outputs.is_duplicate == 'false' && @@ -392,37 +340,17 @@ jobs: steps.parse.outputs.is_valid == 'true' uses: actions/github-script@v7 env: - CLASSROOM_ORG: ${{ vars.CLASSROOM_ORG }} - CLASSROOM_JOIN_URL: ${{ vars.CLASSROOM_JOIN_URL }} DAY1_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY1_ASSIGNMENT_URL }} - CLASSROOM_INVITE_STATUS: ${{ steps.classroom-invite.outputs.status }} with: script: | - const org = (process.env.CLASSROOM_ORG || '').trim(); - const join = (process.env.CLASSROOM_JOIN_URL || '').trim(); const day1 = (process.env.DAY1_ASSIGNMENT_URL || '').trim(); - const inviteStatus = (process.env.CLASSROOM_INVITE_STATUS || '').trim(); const onboarding = []; - if (join) onboarding.push(`- Join classroom: ${join}`); if (day1) onboarding.push(`- Day 1 assignment: ${day1}`); - const orgStatusLines = []; - if (inviteStatus === 'invited') { - orgStatusLines.push(`- We sent an organization invitation for \`${org}\`. Please accept it from your GitHub notifications or email.`); - } else if (inviteStatus === 'already-invited') { - orgStatusLines.push(`- You already have a pending invitation for \`${org}\`. Please accept it before using assignment links.`); - } else if (inviteStatus === 'already-member') { - orgStatusLines.push(`- You are already a member of \`${org}\`.`); - } - const noLinksFallback = onboarding.length ? '' : '\n- Your enrollment was received. A facilitator will share your classroom link shortly.'; - - const orgStatusSection = orgStatusLines.length - ? `## Organization access\n${orgStatusLines.join('\n')}\n\n` - : ''; const successBodyLines = [ '## You are enrolled. Welcome!', '', @@ -440,7 +368,7 @@ jobs: '', 'Please reply `ack` on this issue after you confirm your Day 1 link works.', '', - `${orgStatusSection}If you get stuck at any step, open support:`, + 'If you get stuck at any step, open support:', '- https://github.com/Community-Access/support/issues', '', 'You belong here.' @@ -642,84 +570,21 @@ jobs: labels: ['waitlist'] }); - - name: Invite registrant to classroom organization (optional) - id: classroom-invite - if: >- - steps.dup-check.outputs.is_duplicate == 'false' && - steps.capacity-check.outputs.is_full == 'false' && - vars.CLASSROOM_ORG != '' - uses: actions/github-script@v7 - env: - CLASSROOM_ORG: ${{ vars.CLASSROOM_ORG }} - with: - github-token: ${{ secrets.CLASSROOM_ORG_ADMIN_TOKEN }} - script: | - const org = process.env.CLASSROOM_ORG; - const username = context.payload.issue.user.login; - const userId = context.payload.issue.user.id; - let status = 'skipped'; - - try { - await github.rest.orgs.checkMembershipForUser({ org, username }); - status = 'already-member'; - core.info(`${username} is already a member of ${org}`); - } catch (error) { - if (error.status !== 404) { - throw error; - } - - const invites = await github.paginate(github.rest.orgs.listPendingInvitations, { - org, - per_page: 100 - }); - - const hasPendingInvite = invites.some(invite => - (invite.login || '').toLowerCase() === username.toLowerCase() - ); - - if (hasPendingInvite) { - status = 'already-invited'; - core.info(`${username} already has a pending invite to ${org}`); - } else { - await github.rest.orgs.createInvitation({ - org, - invitee_id: userId, - role: 'direct_member' - }); - status = 'invited'; - core.info(`Sent org invite for ${username} to ${org}`); - } - } - - core.setOutput('status', status); - - name: Post welcome comment if: steps.dup-check.outputs.is_duplicate == 'false' && steps.capacity-check.outputs.is_full == 'false' uses: actions/github-script@v7 env: - CLASSROOM_ORG: ${{ vars.CLASSROOM_ORG }} DAY1_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY1_ASSIGNMENT_URL }} DAY2_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY2_ASSIGNMENT_URL }} - CLASSROOM_INVITE_STATUS: ${{ steps.classroom-invite.outputs.status }} with: script: | - const org = process.env.CLASSROOM_ORG; const day1 = (process.env.DAY1_ASSIGNMENT_URL || '').trim(); const day2 = (process.env.DAY2_ASSIGNMENT_URL || '').trim(); - const inviteStatus = (process.env.CLASSROOM_INVITE_STATUS || '').trim(); let classroomSection = ''; - if (day1 || day2 || inviteStatus) { + if (day1 || day2) { const lines = ['## Classroom Access']; - if (inviteStatus === 'invited') { - lines.push(`- We sent an organization invitation for \`${org}\`. Please accept it from your GitHub notifications or email.`); - } else if (inviteStatus === 'already-invited') { - lines.push(`- You already have a pending invitation for \`${org}\`. Please accept it before using assignment links.`); - } else if (inviteStatus === 'already-member') { - lines.push(`- You are already a member of \`${org}\`.`); - } - if (day1) { lines.push(`- Day 1 assignment link: ${day1}`); } diff --git a/admin/CLASSROOM_INTEGRATION_GUIDE.md b/admin/CLASSROOM_INTEGRATION_GUIDE.md index 96a5e64..c410f9f 100644 --- a/admin/CLASSROOM_INTEGRATION_GUIDE.md +++ b/admin/CLASSROOM_INTEGRATION_GUIDE.md @@ -1,6 +1,6 @@ # GitHub Classroom Integration Guide -This guide helps facilitators connect student enrollment data with GitHub Classroom assignments and team management. +This guide helps facilitators connect student enrollment data with GitHub Classroom assignments and dashboard triage. ## Quick Start: Syncing Students to Classroom @@ -16,23 +16,10 @@ Set these variables in the git-going-with-github repository settings: | Variable | Example | Purpose | |----------|---------|---------| -| `CLASSROOM_ORG` | `git-going-classroom-cohort-2` | Organization where students will be invited | -| `CLASSROOM_JOIN_URL` | `https://classroom.github.com/a/...` | Classroom org join link (optional, posted to students) | | `CLASSROOM_DAY1_ASSIGNMENT_URL` | GitHub Classroom Day 1 assignment link | Posted to students after enrollment | -| `CLASSROOM_DAY2_ASSIGNMENT_URL` | GitHub Classroom Day 2 assignment link | Posted to students after enrollment | -| `CLASSROOM_ORG_ADMIN_TOKEN` | Fine-grained token (see below) | Used to invite students to org | +| `CLASSROOM_DAY2_ASSIGNMENT_URL` | GitHub Classroom Day 2 assignment link | Posted after Day 1 completion signal | -### 3. Create a Fine-Grained Token - -1. Go to Personal Access Tokens: https://github.com/settings/personal-access-tokens/new -2. Name: `Classroom Enrollment Integration` -3. Resource owner: Select your `git-going-classroom-cohort-*` organization -4. Permissions: - - Organization: Members Read/Write (to send invitations) -5. Create and copy the token value -6. Add to repository as secret: `CLASSROOM_ORG_ADMIN_TOKEN` - -### 4. Proficiency-Based Triage +### 3. Proficiency-Based Triage When students enroll, the workflow automatically adds proficiency labels: @@ -40,7 +27,7 @@ When students enroll, the workflow automatically adds proficiency labels: - `proficiency-intermediate` — Some experience - `proficiency-advanced` — Confident users -### 5. Viewing Enrollment Data +### 4. Viewing Enrollment Data - **Public issues**: Redacted for privacy. See [git-going-with-github/issues](https://github.com/Community-Access/git-going-with-github/issues) - **Private intake**: Full details stored in [git-going-with-github-administration](https://github.com/Community-Access/git-going-with-github-administration) @@ -51,8 +38,8 @@ When students enroll, the workflow automatically adds proficiency labels: ### Day 0: Enrollment Opens - Students submit enrollment form -- Workflow validates, redacts public issue, posts success comment with classroom link -- Student receives org invitation +- Workflow validates, redacts public issue, posts success comment with Day 1 assignment link +- Student replies `ack` after confirming Day 1 access ### Day 0-1: Facilitator Triage - Open [enrollment issues with proficiency labels](https://github.com/Community-Access/git-going-with-github/issues?q=label%3Aproficiency-beginner+OR+label%3Aproficiency-intermediate+OR+label%3Aproficiency-advanced) @@ -63,8 +50,8 @@ When students enroll, the workflow automatically adds proficiency labels: - Create teams in classroom org (if using pair programming): - Team: `mentors` (advanced proficiency students) - Team: `learners` (mixed proficiency for support) -- Students accept org invitation from their GitHub notifications - Students access Day 1 assignment via the link posted in their enrollment issue +- Students reply `day1-complete` in the enrollment issue when they complete Day 1 milestone ### Day 1-2: Track Progress - Monitor assignment submissions in GitHub Classroom @@ -72,7 +59,7 @@ When students enroll, the workflow automatically adds proficiency labels: - Re-reach out via [Support Hub Issues](https://github.com/Community-Access/support/issues) ### Day 2: Advanced Assignment -- Link to Day 2 assignment is posted in student enrollment comments (if configured) +- Day 2 assignment link is posted automatically after `day1-complete` signal (label or student comment) - Day 2 focuses on pair programming, code review, and open-source contribution ## Troubleshooting @@ -81,9 +68,9 @@ When students enroll, the workflow automatically adds proficiency labels: - **Cause**: `PRIVATE_STUDENT_DATA_REPO` or `PRIVATE_STUDENT_DATA_TOKEN` not configured - **Fix**: Follow section 2 (Configure Repository Integration) above -### Student didn't receive org invitation -- **Cause**: `CLASSROOM_ORG_ADMIN_TOKEN` is missing or expired -- **Fix**: Create a new fine-grained token (section 3) and update the secret +### Student did not receive Day 2 release +- **Cause**: No Day 1 completion signal found +- **Fix**: Add `day1-complete` comment in enrollment issue or apply `day2-eligible` label ### Can't find enrollment details - **Location**: Private repo → [git-going-with-github-administration/issues](https://github.com/Community-Access/git-going-with-github-administration/issues) diff --git a/admin/ENROLLMENT_SETUP_CHECKLIST.md b/admin/ENROLLMENT_SETUP_CHECKLIST.md index 6903cd6..36e0811 100644 --- a/admin/ENROLLMENT_SETUP_CHECKLIST.md +++ b/admin/ENROLLMENT_SETUP_CHECKLIST.md @@ -2,22 +2,20 @@ Use this checklist to complete the GitHub Classroom integration setup for a new cohort. -## Pre-Enrollment: GitHub Organization Setup +## Pre-Enrollment: GitHub Classroom Setup -- [ ] Create new classroom organization (naming convention: `git-going-classroom-cohort-YYYY-MM`) +- [ ] Confirm classroom access in GitHub Classroom - URL: https://classroom.github.com - - Add co-facilitators as owners - - Enable branch protection rules (optional, recommended for assignment repos) + - Verify facilitators can manage assignments - [ ] Create GitHub Classroom assignments: - [ ] Day 1 assignment (e.g., "Day 1: You Belong Here") - [ ] Day 2 assignment (e.g., "Day 2: You Can Build This") - Get the assignment invitation link from Classroom UI for each -- [ ] Set up teams (optional, for pair programming): - - [ ] Team: `mentors` - - [ ] Team: `learners` - - Add assignment requirements to team permissions +- [ ] Decide your Day 1 completion signal for Day 2 release: + - [ ] Label-based (`day1-complete` or `day2-eligible`) + - [ ] Student comment-based (`day1-complete` in enrollment issue) ## GitHub Repository Configuration @@ -25,21 +23,13 @@ Use this checklist to complete the GitHub Classroom integration setup for a new In [Community-Access/git-going-with-github Settings → Variables](https://github.com/Community-Access/git-going-with-github/settings/variables): -- [ ] `CLASSROOM_ORG` - - Value: `git-going-classroom-cohort-YYYY-MM` - - Purpose: Organization name for student invitations - -- [ ] `CLASSROOM_JOIN_URL` (optional) - - Value: Organization join link from Classroom - - Purpose: Posted to students; skipped if not set - - [ ] `CLASSROOM_DAY1_ASSIGNMENT_URL` - Value: Day 1 Classroom assignment invitation link - Purpose: Posted to students after enrollment; required for Day 1 start - [ ] `CLASSROOM_DAY2_ASSIGNMENT_URL` - Value: Day 2 Classroom assignment invitation link - - Purpose: Posted to students; optional but recommended + - Purpose: Posted to students after Day 1 completion confirmation - [ ] `PRIVATE_STUDENT_DATA_REPO` - Value: `Community-Access/git-going-with-github-administration` @@ -57,13 +47,6 @@ In [Community-Access/git-going-with-github Settings → Variables](https://githu In [Community-Access/git-going-with-github Settings → Secrets](https://github.com/Community-Access/git-going-with-github/settings/secrets/actions): -- [ ] `CLASSROOM_ORG_ADMIN_TOKEN` - - Create: Personal access token (fine-grained) - - Scopes: Organization members (read/write) - - Resource: Your `git-going-classroom-cohort-YYYY-MM` org - - Validity: 1-3 months (typical for cohort duration) - - Action: Update when creating new cohort - - [ ] `PRIVATE_STUDENT_DATA_TOKEN` - Status: Already set ✓ (created at initial setup) - Validity: Verify token has not expired @@ -101,11 +84,8 @@ In [Community-Access/git-going-with-github-administration](https://github.com/Co ## Workshop Day 0: Final Prep -- [ ] Verify all org invitations sent (automation does this automatically) - - Check: https://github.com/orgs/git-going-classroom-cohort-YYYY-MM/people?tab=outside-collaborators - - Pending invitations should show here - -- [ ] Verify assignment links accessible to org members +- [ ] Verify Day 1 assignment link is posted in enrollment confirmation comments +- [ ] Verify `ack` and `day1-complete` guidance is visible to students in comments - [ ] Share facilitator resources: - Classroom Integration Guide: [CLASSROOM_INTEGRATION_GUIDE.md](./CLASSROOM_INTEGRATION_GUIDE.md) @@ -147,7 +127,7 @@ In [Community-Access/git-going-with-github-administration](https://github.com/Co | Problem | Cause | Solution | |---------|-------|----------| | "Enrollment is temporarily unavailable" | Missing secrets/variables | Check this checklist; verify all vars/secrets set | -| Student didn't get org invite | Token expired or missing | Create fresh `CLASSROOM_ORG_ADMIN_TOKEN` secret | +| Student cannot access assignment | Invite link mismatch or not signed in | Re-copy assignment invite URL from Classroom and update variable | | Can't see private intakes | Permission denied | Verify you're an org member of admin repo | | Proficiency labels not appearing | Workflow skipped | Check enrollment issue for errors; re-submit | | Assignments not posting to students | Missing assignment URL variables | Verify `CLASSROOM_DAY1_ASSIGNMENT_URL` is set | diff --git a/admin/FINE_GRAINED_TOKEN_SETUP.md b/admin/FINE_GRAINED_TOKEN_SETUP.md index dd97087..d30f0a2 100644 --- a/admin/FINE_GRAINED_TOKEN_SETUP.md +++ b/admin/FINE_GRAINED_TOKEN_SETUP.md @@ -1,203 +1,52 @@ -# Fine-Grained Token Setup Guide +# Token and Variable Setup -This guide walks you through creating and configuring the fine-grained tokens needed for GitHub Classroom integration. +This guide documents the current credential model for registration and dashboard automation. -## Why Fine-Grained Tokens? +## Registration Flow Credentials -- **Least privilege**: Tokens are scoped to specific repositories and permissions -- **Security**: If exposed, damage is limited to only what that token can access -- **Audit trail**: Personal access tokens can be revoked individually -- **Expiration**: Fine-grained tokens have explicit expiration dates (no endless "magic" tokens) +Registration no longer uses organization invitation tokens. -## Required Tokens +Required variables: -### Token 1: Private Student Data (Read/Write Issues) +- `CLASSROOM_DAY1_ASSIGNMENT_URL` +- `CLASSROOM_DAY2_ASSIGNMENT_URL` -**Purpose**: Write student intake data to private admin repository -**Scope**: Only `Community-Access/git-going-with-github-administration` repository -**Permissions**: Issues (Read and Write) -**Name**: `student-intake-writer` +Required secret for private intake storage: -#### Creating the Token +- `PRIVATE_STUDENT_DATA_TOKEN` -1. Go to: https://github.com/settings/personal-access-tokens/new -2. Configure: - - **Token name**: `student-intake-writer` - - **Expiration**: 90 days (shorter is better; rotate regularly) - - **Description**: Writes student enrollment intake to private admin repo - - **Resource owner**: Select yourself (or your automation account) - - **Permissions**: - - Repository permissions: - - **Issues**: Read and Write +## Dashboard Flow Credential -3. **Resource**: Select "Only select repositories" - - Repository: `Community-Access/git-going-with-github-administration` +`instructor-dashboard-sync.yml` writes progress snapshots to the private administration repository. -4. **Create token** and copy to clipboard +Required secret: -5. Store in source repo (`git-going-with-github`): - ```powershell - gh secret set PRIVATE_STUDENT_DATA_TOKEN \ - --repo Community-Access/git-going-with-github \ - --body "ghp_YOUR_TOKEN_HERE" - ``` +- `INSTRUCTOR_DASHBOARD_TOKEN` -### Token 2: Classroom Organization Member Inviter (Optional) +Recommended scope: -**Purpose**: Send organization invitations to enrolled students -**Scope**: Your classroom organization (e.g., `git-going-classroom-cohort-2025-01`) -**Permissions**: Members (Read and Write) -**Name**: `classroom-member-inviter` +- Repository issues read/write on the private administration repository only. -#### Creating the Token - -1. Go to: https://github.com/settings/personal-access-tokens/new -2. Configure: - - **Token name**: `classroom-member-inviter` - - **Expiration**: 90 days (rotate each cohort) - - **Description**: Sends org member invitations for classroom enrollment - - **Resource owner**: Select yourself (or automation account) - - **Permissions**: - - Organization permissions: - - **Members**: Read and Write - -3. **Resource**: Select "Only select repositories" - - Organization: Select your classroom org (e.g., `git-going-classroom-cohort-2025-01`) - -4. **Create token** and copy to clipboard - -5. Store in source repo: - ```powershell - gh secret set CLASSROOM_ORG_ADMIN_TOKEN \ - --repo Community-Access/git-going-with-github \ - --body "ghp_YOUR_CLASSROOM_TOKEN_HERE" - ``` - -## Verification - -### Check Token 1 (Student Data) - -```powershell -# Verify the token has access -gh api repos/Community-Access/git-going-with-github-administration/issues \ - --jq '.[] | {number, title, labels}' -``` - -If successful, you'll see any existing intake issues. - -### Check Token 2 (Classroom Org) - -```powershell -# Verify the token can list org members -$ORG = "git-going-classroom-cohort-2025-01" -gh api orgs/$ORG/members --jq '.[] | .login' -``` - -If successful, you'll see org members listed. - -## Token Rotation Schedule - -| Token | Frequency | When | Reason | -|-------|-----------|------|--------| -| `student-intake-writer` | Per cohort | After cohort ends | Isolation between cohorts | -| `classroom-member-inviter` | Per cohort | After cohort ends | Org-specific, tie to cohort | - -## Troubleshooting Tokens - -### "Insufficient permissions" error - -**Cause**: Token missing required permission -**Fix**: Create new token with correct permissions; check that **all** required permissions are selected -**Verify**: Test the token scope with `gh api` before storing in secret - -### "Not Found" (404) error - -**Cause**: Token created but not scoped to the right repository/org -**Fix**: Delete and recreate token, ensuring: - - Resource is set to "Only select repositories" - - Correct repository/organization is selected - - Permissions are minimal but sufficient - -### "Unauthorized" (401) error - -**Cause**: Secret stored with typo or token expired -**Fix**: Check expiration date in token page; recreate fresh token if needed -**Verify**: Delete the old secret and set the new one: -```powershell -gh secret delete PRIVATE_STUDENT_DATA_TOKEN --repo Community-Access/git-going-with-github -gh secret set PRIVATE_STUDENT_DATA_TOKEN --body "ghp_NEW_TOKEN_HERE" --repo Community-Access/git-going-with-github -``` - -## Security Best Practices - -1. **Never commit tokens to code** — Always use GitHub secrets or environment variables -2. **Rotate regularly** — Set 90-day expiration on all tokens -3. **Use minimal scopes** — Only grant permissions needed for that specific task -4. **Delete unused tokens** — After rotating, revoke the old token in https://github.com/settings/personal-access-tokens -5. **Use org accounts if possible** — Consider automation account separate from personal GitHub -6. **Audit token usage** — Check [Token audit log](https://github.com/settings/personal-access-tokens) monthly - -## Variables Needed (No Secrets) - -These are set as repository variables (not secrets) because they're not sensitive: +## Verification Commands ```powershell -# Classroom organization name -gh variable set CLASSROOM_ORG --body "git-going-classroom-cohort-2025-01" \ - --repo Community-Access/git-going-with-github - -# Optional: Organization join link -gh variable set CLASSROOM_JOIN_URL \ - --body "https://classroom.github.com/a/..." \ - --repo Community-Access/git-going-with-github - -# Optional: Day 1 assignment link -gh variable set CLASSROOM_DAY1_ASSIGNMENT_URL \ - --body "https://classroom.github.com/a/..." \ - --repo Community-Access/git-going-with-github - -# Optional: Day 2 assignment link -gh variable set CLASSROOM_DAY2_ASSIGNMENT_URL \ - --body "https://classroom.github.com/a/..." \ - --repo Community-Access/git-going-with-github - -# Already set: private data storage -gh variable set PRIVATE_STUDENT_DATA_REPO \ - --body "Community-Access/git-going-with-github-administration" \ - --repo Community-Access/git-going-with-github - -# Already set: export controls -gh variable set ENABLE_PUBLIC_CLASSROOM_INTAKE_EXPORT --body "false" \ - --repo Community-Access/git-going-with-github - -gh variable set ENABLE_PUBLIC_REGISTRATION_EXPORT --body "false" \ - --repo Community-Access/git-going-with-github +gh variable list -R Community-Access/git-going-with-github +gh secret list -R Community-Access/git-going-with-github ``` -## Quick Reference: All Required Variables & Secrets - -### Secrets (Hidden) -- `PRIVATE_STUDENT_DATA_TOKEN` — Fine-grained token for writing to admin repo -- `CLASSROOM_ORG_ADMIN_TOKEN` — Fine-grained token for org member invites (optional) - -### Variables (Visible) -- `PRIVATE_STUDENT_DATA_REPO` — `Community-Access/git-going-with-github-administration` -- `ENABLE_PUBLIC_CLASSROOM_INTAKE_EXPORT` — `false` -- `ENABLE_PUBLIC_REGISTRATION_EXPORT` — `false` -- `CLASSROOM_ORG` — Your cohort org name (e.g., `git-going-classroom-cohort-2025-01`) -- `CLASSROOM_DAY1_ASSIGNMENT_URL` — Classroom assignment link (optional) -- `CLASSROOM_DAY2_ASSIGNMENT_URL` — Classroom assignment link (optional) +Expected registration-related output: -## Next Steps +- Variables include `CLASSROOM_DAY1_ASSIGNMENT_URL` and `CLASSROOM_DAY2_ASSIGNMENT_URL`. +- Secrets include `PRIVATE_STUDENT_DATA_TOKEN`. +- If dashboard sync is enabled, secrets include `INSTRUCTOR_DASHBOARD_TOKEN`. -Once tokens are set up: +## Rotation Guidance -1. Create a test enrollment: submit a [Classroom Enrollment](https://github.com/Community-Access/git-going-with-github/issues/new?template=classroom-enrollment.yml) issue -2. Verify workflow succeeded: Check [Actions → Registration workflow](https://github.com/Community-Access/git-going-with-github/actions/workflows/registration.yml) -3. Check private repo: Verify intake issue created in [git-going-with-github-administration](https://github.com/Community-Access/git-going-with-github-administration/issues) -4. Verify org invite: Check org members pending list in [Classroom org](https://github.com/settings/organizations) +Rotate tokens at least once per cohort. ---- +Rotation order: -**Last updated**: 2026-05-12 -**Contact**: Facilitator lead or repo maintainer for questions +1. Create new token. +2. Update repository secret. +3. Run a smoke test workflow. +4. Revoke old token. diff --git a/admin/GITHUB_CLASSROOM_ARCHITECTURE.md b/admin/GITHUB_CLASSROOM_ARCHITECTURE.md index 90dff3f..a6c47d0 100644 --- a/admin/GITHUB_CLASSROOM_ARCHITECTURE.md +++ b/admin/GITHUB_CLASSROOM_ARCHITECTURE.md @@ -1,300 +1,70 @@ -# GitHub Classroom Integration: Architecture & Magic +# GitHub Classroom Integration Architecture -This document explains the complete automation architecture for GitHub Classroom integration and what makes it "magical" for students and facilitators. +This document describes the current production architecture for registration and classroom handoff. -## The Magic: What Happens Automatically +## Design Goals -### When a Student Submits an Enrollment +- Keep enrollment automation in a public repository without exposing private intake details. +- Avoid organization invitation dependencies. +- Use deterministic issue signals for Day 2 release and dashboard status. -1. **Instant Validation** — Form fields checked for completeness in < 1 second -2. **Duplicate Detection** — System checks if student already enrolled, prevents duplicates -3. **Private Storage** — Full enrollment data (email, goals, proficiency levels) stored in secure private repo automatically -4. **Public Redaction** — Student's PII removed from public issue, replaced with clean summary -5. **Proficiency Tagging** — Automatic labels added based on GitHub/VS Code experience for facilitator triage -6. **Org Invitation** — Automatic invite sent to classroom organization -7. **Assignment Links Posted** — Day 1 & Day 2 assignment URLs posted to student -8. **Enrollment Closed** — Issue marked complete, no clutter for students +## High-Level Flow -**Total time**: ~5 seconds, fully automated, zero facilitator intervention needed +1. Student opens enrollment or registration issue. +2. `registration.yml` validates submission and posts assignment links. +3. Student replies `ack` after Day 1 link verification. +4. Student replies `day1-complete` after Day 1 milestone. +5. `day2-release.yml` posts Day 2 assignment link when completion signal is present. +6. `instructor-dashboard-sync.yml` writes status snapshots to private admin repo issues. -### What Students See +## Core Workflows -They get a friendly, actionable welcome comment with: -- ✓ Confirmation their enrollment was accepted -- ✓ Organization invitation status -- ✓ Direct links to Day 1 assignment -- ✓ Link to Day 2 assignment (after Day 1) -- ✓ Support link if they get stuck +### `registration.yml` -And then the issue closes — no notifications, clean, professional. +- Handles duplicate and waitlist behavior. +- Stores detailed intake in private repo when configured. +- Redacts public issue body for privacy. +- Posts Day 1 and Day 2 links from repository variables. -### What Facilitators See +### `day2-release.yml` -**Public view** (no PII): Issues with proficiency labels for triage -- Filter by `label:proficiency-beginner` to find students who need extra support -- Filter by `label:proficiency-advanced` to identify peer mentors +- Runs on schedule and manual dispatch. +- Reads enrollment issues with `enrolled` label. +- Releases Day 2 when either condition is true: + - Label `day2-eligible` or `day1-complete` is present. + - Student comment contains `day1-complete`. -**Private admin repo**: Full student data when needed -- Names, emails, stated goals, and proficiency levels -- Private issue link connects to public record -- Filterable by `label:intake` or `label:classroom-enrollment` -- Easy CSV export if needed for external tools +### `instructor-dashboard-sync.yml` -## System Architecture +- Reads enrollment issues and comments from this repo. +- Computes status categories: + - `awaiting-ack` + - `active-day1` + - `day1-complete` + - `day2-released` + - `needs-info` +- Upserts one dashboard issue per student in the private admin repo. -``` -┌─────────────────────────────────────────────────────────────┐ -│ Student Enrollment Experience │ -├─────────────────────────────────────────────────────────────┤ -│ │ -│ 1. Student fills form (Public GitHub Issue) │ -│ └─→ .github/ISSUE_TEMPLATE/classroom-enrollment.yml │ -│ │ -│ 2. Workflow triggers (on: issues → opened) │ -│ └─→ .github/workflows/registration.yml │ -│ [classroom-enrollment job] │ -│ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Step 1: Duplicate Check │ │ -│ │ Query: GitHub username in prior enrollments │ │ -│ │ Result: Skip if duplicate, error message │ │ -│ └─────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Step 2: Guardrail Check │ │ -│ │ Verify: PRIVATE_STUDENT_DATA_REPO exists │ │ -│ │ Verify: PRIVATE_STUDENT_DATA_TOKEN valid │ │ -│ │ If missing: Error + admin alert │ │ -│ └─────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Step 3: Parse & Validate │ │ -│ │ Extract: Full Name, Email, GitHub Handle │ │ -│ │ Validate: Required fields present │ │ -│ │ If invalid: Request user fix + retry │ │ -│ └─────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Step 4: Private Storage [PARALLEL] │ │ -│ │ Store: Full intake in admin repo │ │ -│ │ Label: "intake", "classroom-enrollment" │ │ -│ │ Resilience: Handles missing labels │ │ -│ └─────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Step 5: Public Redaction [PARALLEL] │ │ -│ │ Replace: Issue body with clean summary │ │ -│ │ Remove: All PII (name, email, goals) │ │ -│ │ Keep: Status, submitted-by, timestamp │ │ -│ └─────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Step 6: Proficiency Labeling │ │ -│ │ Extract: GitHub experience level │ │ -│ │ Extract: VS Code experience level │ │ -│ │ Label: proficiency-{beginner,intermediate, │ │ -│ │ advanced,unassessed} │ │ -│ └─────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Step 7: Org Invitation (if configured) │ │ -│ │ Check: User already member? Skip │ │ -│ │ Check: Pending invite exists? Skip │ │ -│ │ Action: Send invite to CLASSROOM_ORG │ │ -│ │ Token: CLASSROOM_ORG_ADMIN_TOKEN (optional) │ │ -│ └─────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Step 8: Post Success Comment │ │ -│ │ Display: Org invite status │ │ -│ │ Link: Day 1 assignment (if configured) │ │ -│ │ Link: Day 2 assignment (if configured) │ │ -│ │ Link: Support hub │ │ -│ └─────────────────────────────────────────────┘ │ -│ ↓ │ -│ ┌─────────────────────────────────────────────┐ │ -│ │ Step 9: Add Labels & Close │ │ -│ │ Label: "enrolled" │ │ -│ │ State: Closed with "completed" reason │ │ -│ │ Result: Clean, no follow-up needed │ │ -│ └─────────────────────────────────────────────┘ │ -│ │ -│ 3. Student receives: │ -│ • Org invitation in GitHub notifications │ -│ • Welcome comment with next steps │ -│ • Direct link to Day 1 assignment │ -│ │ -│ 4. Facilitator sees: │ -│ • Public issue (no PII) with proficiency label │ -│ • Private intake record with full details │ -│ • Enrollment summary in dashboards (if enabled) │ -│ │ -└─────────────────────────────────────────────────────────────┘ -``` +## Configuration Surface -## Data Flow & Privacy +### Variables -### Public Path (No PII) -``` -Student Issue (redacted) - └─→ Proficiency labels → Facilitator triage - └─→ Closed, marked "enrolled" - └─→ Used for: Counting, labeling, matching with private records -``` +- `CLASSROOM_DAY1_ASSIGNMENT_URL` +- `CLASSROOM_DAY2_ASSIGNMENT_URL` +- `PRIVATE_STUDENT_DATA_REPO` +- `ENABLE_PUBLIC_CLASSROOM_INTAKE_EXPORT` +- `ENABLE_PUBLIC_REGISTRATION_EXPORT` -### Private Path (Full PII) -``` -Enrollment Data (full) - └─→ Private Admin Repo issue - └─→ Secured by: GitHub org settings + private repo access list - └─→ Used by: Facilitators for contact, accommodations, team assignments -``` +### Secrets -### Cross-Link -``` -Public issue ↔ Private issue (link in private issue body) - └─→ Facilitators can click from admin repo to public record - └─→ Students never see private repo - └─→ Clean separation of concerns -``` +- `PRIVATE_STUDENT_DATA_TOKEN` +- `INSTRUCTOR_DASHBOARD_TOKEN` (dashboard sync) -## The "Magical" Features Explained +## Student Interaction Contract -### 1. Automatic Proficiency Profiling +Required student replies: -**What**: System reads "GitHub Experience" and "VS Code Experience" fields, converts them to proficiency labels. +- `ack` after opening Day 1 assignment. +- `day1-complete` after Day 1 milestone completion. -**Magic**: Facilitators can instantly create teams by skill level. -- Filter: `label:proficiency-advanced` -- Select: Potential peer mentors -- Create: `mentors` team → assign to Day 2 pair programming - -**Result**: No manual form reading, no spreadsheet copy-pasting, instant team assignment. - ---- - -### 2. Zero-Intervention Org Invites - -**What**: System automatically sends organization invitations using GitHub API. - -**Magic**: Students accept invite from notification → immediately have classroom org access → can submit assignments without any facilitator interaction. - -**Result**: Student friction reduced from "wait for facilitator email" to "accept GitHub notification" (~30 seconds). - ---- - -### 3. Assignment Link Delivery - -**What**: Day 1 and Day 2 assignment links posted directly to student's enrollment issue. - -**Magic**: Student gets one place to find everything: -- Enrollment confirmation -- Org status -- Day 1 assignment link -- Support link - -**Result**: No email, no Slack, no "where do I go?", just one clean comment with everything they need. - ---- - -### 4. Private PII Storage with Public Redaction - -**What**: Full enrollment data stored privately; public issue redacted automatically. - -**Magic**: Facilitators can: -- See full names, emails, stated goals → useful for personalization -- Filter by stated goals → identify students with specific interests -- Track accessibility needs → accommodations noted in private record -- While public issue remains anonymous → student privacy protected - -**Result**: GDPR-compliant, privacy-respecting, facilitator-friendly. - ---- - -### 5. Proficiency-Driven Facilitator Triage - -**What**: GitHub and VS Code experience levels converted to machine-readable labels. - -**Magic**: Facilitators use GitHub's native label filtering: -``` -Filter: label:proficiency-beginner -→ "Show me all students new to GitHub/VS Code" -→ Plan extra setup help, pair them with mentors, provide scaffolding -``` - -**Result**: Differentiated instruction without manual intake form reading. - ---- - -### 6. Duplicate Prevention - -**What**: System checks if GitHub username already has a prior enrollment. - -**Magic**: Students can't accidentally submit twice: -- Prevents duplicate org invites -- Prevents duplicate data storage -- Prevents confusion about which enrollment is "real" - -**Result**: Clean enrollment roster, no data cleanup needed. - ---- - -## Configuration Overview - -| Component | File | Purpose | -|-----------|------|---------| -| Form | `.github/ISSUE_TEMPLATE/classroom-enrollment.yml` | Beginner-friendly input (6 fields) | -| Workflow | `.github/workflows/registration.yml` | Orchestration (validation → storage → labeling → invites → welcome) | -| Private Repo | `Community-Access/git-going-with-github-administration` | Secure PII storage + facilitator dashboard | -| Variables | Repository settings | Classroom org, assignment links, export controls | -| Secrets | Repository settings | API tokens for org invites + private storage | -| Documentation | `admin/*.md` | Setup guides, checklists, troubleshooting | - -## Optional Enhancements (Future) - -These are "nice-to-haves" that could further increase magic: - -1. **Assignment Submission Webhooks** - - Monitor when students complete Day 1 assignment - - Auto-tag in private intake: `completed:day-1` - - Facilitators can filter: `label:completed:day-1 NOT label:completed:day-2` → identify Day 2 laggards - -2. **Student Success Dashboard** - - Real-time status: Enrolled → Accepted invite → Day 1 started → Day 1 submitted → Day 2 started → etc. - - Color-coded risk: Red (lagging), Yellow (on-track), Green (completed) - -3. **Auto-Generated Team Assignments** - - Workflow creates a `TEAM_ASSIGNMENTS.md` in admin repo - - Shows: Mentors → Assigned learners, experience mix notes - -4. **Facilitator Bulk Actions** - - "Award badges" to students who complete milestones - - Automatically post to their enrollment issue: "🏆 Completed Day 1!" - -5. **Integration with Learning Room** - - Auto-create learning-room-specific issues for each cohort - - Link cohort roster to learning room progress tracking - -## Success Metrics - -When GitHub Classroom integration is working well, you'll see: - -✓ **100% enrollment processed** in < 5 seconds -✓ **0% data exposure** (no PII in public) -✓ **100% org invites delivered** (automated, no manual outreach) -✓ **< 24 hour turnaround** from enrollment → classroom ready -✓ **Facilitators report** proficiency labels useful for triage -✓ **Students report** clear next steps, no confusion - -## Troubleshooting & Support - -- **Setup issues?** → See [FINE_GRAINED_TOKEN_SETUP.md](./FINE_GRAINED_TOKEN_SETUP.md) -- **Configuration questions?** → See [ENROLLMENT_SETUP_CHECKLIST.md](./ENROLLMENT_SETUP_CHECKLIST.md) -- **Classroom integration?** → See [CLASSROOM_INTEGRATION_GUIDE.md](./CLASSROOM_INTEGRATION_GUIDE.md) -- **Facilitator how-tos?** → See [FACILITATOR_GUIDE.md](./FACILITATOR_GUIDE.md) - ---- - -**Version**: 1.0 -**Last Updated**: 2026-05-12 -**Maintained by**: Community-Access facilitator team +These text signals are intentionally simple so facilitators can test and recover quickly. diff --git a/admin/IMPLEMENTATION_SUMMARY.md b/admin/IMPLEMENTATION_SUMMARY.md index 8f5edcc..7a7872b 100644 --- a/admin/IMPLEMENTATION_SUMMARY.md +++ b/admin/IMPLEMENTATION_SUMMARY.md @@ -1,235 +1,66 @@ -# GitHub Classroom Integration: Complete Implementation Summary - -**Date**: 2026-05-12 -**Status**: Ready for testing -**Tested**: Configuration verified ✓ - -## What Was Done - -This implementation provides a complete, automated enrollment-to-classroom pipeline with privacy-by-design, zero facilitator intervention, and GitHub Classroom "magic." - -### 1. Privacy Hardening (Completed ✓) - -#### Legacy Registration Flow -- Added `ENABLE_PUBLIC_REGISTRATION_EXPORT` variable (default: `false`) -- Legacy CSV export now gated by this flag -- Students still welcome via old form, but no public data dump -- Privacy parity: New and legacy paths both secure by default - -#### Enrollment Flow -- Automatic private PII storage to `Community-Access/git-going-with-github-administration` -- Automatic public redaction (PII removed from public issue) -- Clean separation: Public sees proficiency labels only -- Facilitators see full data in private repo only - -### 2. GitHub Classroom Integration (Completed ✓) - -#### Automatic Features -1. **Proficiency Labeling** - - Extracts GitHub and VS Code experience levels from form - - Converts to labels: `proficiency-beginner`, `proficiency-intermediate`, `proficiency-advanced`, `proficiency-unassessed` - - Enables instant facilitator triage - -2. **Organization Invitations** - - Automatic invite sent to `CLASSROOM_ORG` - - Checks for duplicates & pending invites - - Uses `CLASSROOM_ORG_ADMIN_TOKEN` (optional fine-grained token) - -3. **Assignment Links** - - Day 1 and Day 2 assignment URLs posted to student - - Comes from variables: `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL` - - Student gets everything in one place - -4. **Success Messaging** - - Friendly, actionable welcome comment - - Shows org invitation status - - Links to assignments, support, and resources - - Issue auto-closes after processing - -### 3. Configuration & Controls (Completed ✓) - -#### Repository Variables -``` -PRIVATE_STUDENT_DATA_REPO = "Community-Access/git-going-with-github-administration" -ENABLE_PUBLIC_CLASSROOM_INTAKE_EXPORT = "false" -ENABLE_PUBLIC_REGISTRATION_EXPORT = "false" -``` - -#### Repository Secrets -``` -PRIVATE_STUDENT_DATA_TOKEN = [TOKEN] (broad scope currently, needs upgrade) -``` - -#### Optional Classroom Variables (To Be Configured By Facilitator) -``` -CLASSROOM_ORG = "git-going-classroom-cohort-YYYY-MM" -CLASSROOM_JOIN_URL = "https://classroom.github.com/a/..." (optional) -CLASSROOM_DAY1_ASSIGNMENT_URL = "https://classroom.github.com/a/..." (optional) -CLASSROOM_DAY2_ASSIGNMENT_URL = "https://classroom.github.com/a/..." (optional) -``` - -#### Optional Classroom Secret (To Be Configured By Facilitator) -``` -CLASSROOM_ORG_ADMIN_TOKEN = [FINE-GRAINED TOKEN] (optional, for org invites) -``` - -### 4. Documentation Created (Completed ✓) - -| Document | Purpose | Audience | -|----------|---------|----------| -| `admin/GITHUB_CLASSROOM_ARCHITECTURE.md` | System overview, data flow, magic explained | Facilitators, maintainers | -| `admin/CLASSROOM_INTEGRATION_GUIDE.md` | Step-by-step setup and troubleshooting | Facilitators, implementers | -| `admin/ENROLLMENT_SETUP_CHECKLIST.md` | Pre/during/post workshop checklists | Facilitators | -| `admin/FINE_GRAINED_TOKEN_SETUP.md` | Token creation and best practices | Maintainers, security | - -### 5. Workflow Enhancements (Completed ✓) - -#### classroom-enrollment Job -- ✓ Duplicate detection by GitHub username -- ✓ Storage guardrail (prevents accidental public storage) -- ✓ Field parsing & validation -- ✓ Private intake creation (resilient, handles missing labels) -- ✓ Public body redaction -- ✓ Proficiency-based auto-labeling -- ✓ Org member invitations (optional, if token provided) -- ✓ Success comment with assignments (optional, if URLs provided) -- ✓ Issue closed with "completed" reason - -#### export-csv Job -- ✓ Now gated by `ENABLE_PUBLIC_REGISTRATION_EXPORT` flag -- ✓ Disabled by default for privacy - -## What Makes It "Magical" - -### For Students -1. **One-step enrollment** — Fill form once, automation does everything else -2. **Instant confirmation** — Welcome comment appears within seconds -3. **Ready to code** — Assignment link in same comment, no email hunting -4. **Clear next steps** — No ambiguity, no "now what?" - -### For Facilitators -1. **Zero intervention** — No manual invites, no follow-ups, fully automated -2. **Instant triage** — Proficiency labels available immediately -3. **Privacy compliant** — PII stored privately, students' public records clean -4. **Duplicate prevention** — No data cleanup needed -5. **Scalable** — Works for 5 students or 500 students identically - -### For Maintainers -1. **Secure by default** — PII storage gated, public export disabled, tokens scoped -2. **Audit trail** — Intake stored in private repo, linked to public record -3. **Flexible** — All classroom features optional, adapts to different orgs/workflows -4. **Documented** — Complete architecture guides, setup checklists, troubleshooting - -## Current Status: Next Actions for Facilitator - -### Immediate (Before First Enrollment) - -1. **Create Fine-Grained Token** (Security best practice) - - Follow: [admin/FINE_GRAINED_TOKEN_SETUP.md](./FINE_GRAINED_TOKEN_SETUP.md) - - Replace current broad token with: - - `PRIVATE_STUDENT_DATA_TOKEN`: Scoped to admin repo, Issues R/W only - - (Optional) `CLASSROOM_ORG_ADMIN_TOKEN`: Scoped to classroom org, Members R/W only - -2. **Configure Classroom Variables** - - Follow: [admin/ENROLLMENT_SETUP_CHECKLIST.md](./ENROLLMENT_SETUP_CHECKLIST.md) - - Set: `CLASSROOM_ORG`, `CLASSROOM_DAY1_ASSIGNMENT_URL`, `CLASSROOM_DAY2_ASSIGNMENT_URL` - - Verify: Test with `gh variable list` - -### Day-of-Workshop (Before Opening Enrollment) - -3. **Verify All Configuration** - - Follow: [admin/ENROLLMENT_SETUP_CHECKLIST.md](./ENROLLMENT_SETUP_CHECKLIST.md) → "Workshop Day 0" - - Verify: Org invitations deliver successfully - - Test: Submit a test enrollment, check workflow run - -### During Workshop - -4. **Monitor Enrollment** - - Filter by proficiency labels to identify students needing support - - Use private admin repo to track full student data - - Monitor assignment submission (if tracking enabled) - -### After Workshop - -5. **Rotate Tokens** - - Create fresh tokens for next cohort - - Revoke old tokens in https://github.com/settings/personal-access-tokens - -## Testing Checklist - -Before going live with first real cohort: - -- [ ] Submit test enrollment through form -- [ ] Verify workflow runs successfully (check Actions) -- [ ] Verify private intake created in admin repo -- [ ] Verify public issue redacted (no PII visible) -- [ ] Verify proficiency labels applied -- [ ] Verify org invitation sent (check pending invites in org) -- [ ] Verify success comment posted with assignment links -- [ ] Verify issue closed with "completed" reason -- [ ] Test org invite acceptance (from test account) -- [ ] Verify student can access classroom assignment - -## Known Limitations & Future Enhancements - -### Current Limitations -- Org invitations sent but can't auto-accept (GitHub limitation) -- Assignment submissions not auto-tracked (would need webhook integration) -- No auto-team assignment (could add future job) -- No grade syncing (would need Classroom API) - -### Potential Future Enhancements -1. **Assignment Submission Tracking** — Auto-label intakes when Day 1/Day 2 submitted -2. **Progress Dashboard** — Real-time cohort status (enrolled → invited → started → completed) -3. **Auto-Team Assignments** — Generate team suggestions based on proficiency -4. **Accessibility Requests** — Auto-compile accessibility notes from intakes -5. **Communicable Milestones** — Award badges on enrollment issue when student completes assignments - -## Support & Questions - -- **Setup help?** → [admin/ENROLLMENT_SETUP_CHECKLIST.md](./ENROLLMENT_SETUP_CHECKLIST.md) -- **Token questions?** → [admin/FINE_GRAINED_TOKEN_SETUP.md](./FINE_GRAINED_TOKEN_SETUP.md) -- **Architecture overview?** → [admin/GITHUB_CLASSROOM_ARCHITECTURE.md](./GITHUB_CLASSROOM_ARCHITECTURE.md) -- **Workflow questions?** → [admin/CLASSROOM_INTEGRATION_GUIDE.md](./CLASSROOM_INTEGRATION_GUIDE.md) -- **Bug reports?** → File issue in [Community-Access/support](https://github.com/Community-Access/support/issues) - ---- +# Registration + Classroom Integration Summary -## Files Modified/Created - -### Workflow -- `.github/workflows/registration.yml` — Updated classroom-enrollment job with proficiency labeling + optional org invites - -### Form -- `.github/ISSUE_TEMPLATE/classroom-enrollment.yml` — (No changes, already complete) - -### Documentation Created -- `admin/GITHUB_CLASSROOM_ARCHITECTURE.md` — System overview -- `admin/CLASSROOM_INTEGRATION_GUIDE.md` — Setup & operation guide -- `admin/ENROLLMENT_SETUP_CHECKLIST.md` — Step-by-step checklist -- `admin/FINE_GRAINED_TOKEN_SETUP.md` — Token creation & security -- `admin/IMPLEMENTATION_SUMMARY.md` — This file - -### Configuration -- Variables: `ENABLE_PUBLIC_REGISTRATION_EXPORT=false` (added) -- Variables: `ENABLE_PUBLIC_CLASSROOM_INTAKE_EXPORT=false` (already set) -- Variables: `PRIVATE_STUDENT_DATA_REPO=...` (already set) -- Secrets: `PRIVATE_STUDENT_DATA_TOKEN` (already set) +Date: 2026-05-12 -## Ready to Deploy? +## Active Model -✓ **Workflow logic**: Complete and tested -✓ **Privacy controls**: In place (public export disabled, private storage gated) -✓ **GitHub Classroom integration**: Automatic org invites, proficiency labels, assignment links -✓ **Documentation**: Comprehensive setup guides and architecture docs -⚠ **Token upgrade**: Pending (move from broad to fine-grained scope) -⚠ **Classroom configuration**: Pending (facilitator to add org + assignment URLs) - -**Status**: Ready for facilitator configuration and testing +The repository now uses assignment-link automation without organization invitation automation. ---- +### Student interaction sequence -**Implementation Date**: 2026-05-12 -**Implemented By**: Automation & Architecture -**Reviewed By**: [Pending] -**Approved For Production**: [Pending] +1. Student submits enrollment issue. +2. Workflow validates and posts Day 1 assignment link. +3. Student replies `ack` after confirming access. +4. Student replies `day1-complete` (or facilitator applies `day2-eligible`). +5. Day 2 release workflow posts Day 2 assignment link. + +## Configuration Required + +### Variables + +- `CLASSROOM_DAY1_ASSIGNMENT_URL` +- `CLASSROOM_DAY2_ASSIGNMENT_URL` +- `PRIVATE_STUDENT_DATA_REPO` +- `ENABLE_PUBLIC_CLASSROOM_INTAKE_EXPORT` +- `ENABLE_PUBLIC_REGISTRATION_EXPORT` + +### Secrets + +- `PRIVATE_STUDENT_DATA_TOKEN` +- `INSTRUCTOR_DASHBOARD_TOKEN` (for dashboard sync) + +## Workflow Changes + +- `registration.yml` + - Removed org invite dependency. + - Removed classroom join variable dependency. + - Keeps duplicate/waitlist behavior. + - Keeps assignment-link posting. + +- `day2-release.yml` + - Uses issue signals (`day1-complete` or `day2-eligible`) to release Day 2. + - No org API dependency. + +- `instructor-dashboard-sync.yml` + - Uses enrollment issue labels/comments for status snapshots. + - No classroom org scan dependency. + +## QA and Testing Updates + +Updated documents: + +- `GO-LIVE-QA-GUIDE.md` +- `admin/LEARNING-ROOM-E2E-QA-RUNBOOK.md` +- `admin/ENROLLMENT_SETUP_CHECKLIST.md` +- `admin/REGISTRATION-QUICKSTART.md` +- `admin/REGISTRATION-ADMIN.md` +- `admin/QUICK_START_SETUP.md` +- `admin/CLASSROOM_INTEGRATION_GUIDE.md` + +## Readiness Checklist + +- Day 1 link variable set and validated. +- Day 2 link variable set and validated. +- Student test confirms `ack` and `day1-complete` path. +- Day 2 release comment verified. +- Dashboard sync workflow produces student status issues in admin repo. diff --git a/admin/QUICK_START_SETUP.md b/admin/QUICK_START_SETUP.md index 699d38b..de1d9f4 100644 --- a/admin/QUICK_START_SETUP.md +++ b/admin/QUICK_START_SETUP.md @@ -1,128 +1,59 @@ -# Quick Start: Get GitHub Classroom Integration Running in 30 Minutes +# Quick Start: Assignment-Link Registration Setup -This quick start walks you through setup in the fastest possible way. +This quick start configures registration automation for the current student interaction model. ## Prerequisites -- GitHub Classroom org created (or use existing): https://classroom.github.com -- Admin access to Community-Access/git-going-with-github repo -- Day 1 & Day 2 assignments created in Classroom (or have their links ready) -## Step 1: Create Your Classroom Organization (5 min) +- Classroom assignments exist in GitHub Classroom. +- You have admin access to `Community-Access/git-going-with-github`. +- You can copy Day 1 and Day 2 assignment invite links. -Skip if already done. +## Step 1: Copy Assignment Invite Links -1. Go: https://classroom.github.com -2. Create org: `git-going-classroom-cohort-YYYY-MM` -3. Add facilitators as owners -4. Create Day 1 & Day 2 assignments, save links +From `https://classroom.github.com`: -## Step 2: Configure Repository Variables (5 min) +1. Open Day 1 assignment and copy invite link. +2. Open Day 2 assignment and copy invite link. -Go: https://github.com/Community-Access/git-going-with-github/settings/variables +Expected format: -Create/update these variables: +- `https://classroom.github.com/a/` -| Name | Value | -|------|-------| -| `CLASSROOM_ORG` | Your org name, e.g. `git-going-classroom-cohort-2025-01` | -| `CLASSROOM_DAY1_ASSIGNMENT_URL` | Your Day 1 Classroom link (e.g. `https://classroom.github.com/a/xyz123`) | -| `CLASSROOM_DAY2_ASSIGNMENT_URL` | Your Day 2 Classroom link (e.g. `https://classroom.github.com/a/abc456`) | -| `CLASSROOM_JOIN_URL` | (Optional) Classroom org join link | - -Click "New repository variable" for each, paste value, click Add. - -## Step 3: Create Classroom Org Token (10 min) - -This allows the automation to send org member invitations. - -1. Go: https://github.com/settings/personal-access-tokens/new -2. Fill: - - **Token name**: `classroom-member-inviter` - - **Expiration**: 90 days - - **Permissions**: Check "Organization → Members → Read and Write" - - **Resource**: Select your `git-going-classroom-cohort-YYYY-MM` org only -3. Click "Create token" -4. Copy the token value (starts with `ghp_`) - -Now store it as a secret: - -Go: https://github.com/Community-Access/git-going-with-github/settings/secrets/actions - -Click "New repository secret": -- **Name**: `CLASSROOM_ORG_ADMIN_TOKEN` -- **Value**: Paste your token -- Click "Add secret" - -## Step 4: Test (5 min) +## Step 2: Configure Repository Variables -1. Go: https://github.com/Community-Access/git-going-with-github/issues/new?template=classroom-enrollment.yml -2. Fill out the form with your own info -3. Submit -4. Watch the automation: - - Go: https://github.com/Community-Access/git-going-with-github/actions - - Click latest "Registration" run - - Watch steps complete +Go to `https://github.com/Community-Access/git-going-with-github/settings/variables/actions`. -Expected results: -- ✓ Issue body redacted (shows only "[ENROLL-INTAKE]" summary, no PII visible) -- ✓ Proficiency labels added (look for `proficiency-beginner`, etc.) -- ✓ Org invitation sent (check: https://github.com/orgs/YOUR_ORG/people?tab=outside-collaborators) -- ✓ Success comment posted with assignment links -- ✓ Issue closed +Create or update: -## Step 5: Go Live! (0 min) - -You're done. Share the enrollment link with students: - -> "Ready to join the workshop? Go here to enroll: https://github.com/Community-Access/git-going-with-github/issues/new?template=classroom-enrollment.yml" - -Students will see the form, submit, and automation takes it from there. - ---- - -## What Happens Automatically Now - -When a student enrolls: -1. ✓ Form validated (required fields checked) -2. ✓ Org invitation sent (instantly) -3. ✓ Full data saved (private repo, no PII exposed) -4. ✓ Proficiency tagged (for facilitator triage) -5. ✓ Welcome comment posted (with assignment links) -6. ✓ Issue closed (clean, professional) +| Name | Value | +|---|---| +| `CLASSROOM_DAY1_ASSIGNMENT_URL` | Day 1 invite link | +| `CLASSROOM_DAY2_ASSIGNMENT_URL` | Day 2 invite link | -**Time to ready**: ~5 seconds -**Facilitator intervention needed**: 0% +## Step 3: Validate Registration Flow -## Facilitator Next Steps +1. Open a test enrollment issue. +2. Confirm welcome comment includes Day 1 link. +3. Reply `ack` from test student account. +4. Reply `day1-complete` from test student account. +5. Confirm Day 2 release comment appears. -### Immediately -- [ ] Monitor who's enrolling: https://github.com/Community-Access/git-going-with-github/issues?label=classroom-enrollment -- [ ] Filter by proficiency: https://github.com/Community-Access/git-going-with-github/issues?label=proficiency-beginner -- [ ] Create "mentors" team from advanced students (optional) +## Expected Results -### During Workshop -- [ ] Verify students accepted org invite -- [ ] Re-reach out to any who haven't started Day 1 assignment -- [ ] Use private admin repo for tracking: https://github.com/Community-Access/git-going-with-github-administration/issues?label=intake +- Enrollment issue is processed successfully. +- No org-invite dependency exists. +- Day 2 release is driven by issue signal (`day1-complete` or `day2-eligible`). ## Troubleshooting | Problem | Fix | -|---------|-----| -| Org invitations not sending | Check `CLASSROOM_ORG_ADMIN_TOKEN` secret is set correctly | -| Assignment links not posting | Check `CLASSROOM_DAY1_ASSIGNMENT_URL` and `CLASSROOM_DAY2_ASSIGNMENT_URL` are set | -| Proficiency labels not appearing | Check workflow ran successfully in Actions tab | -| Students don't see enrollment form link | Share this URL: https://github.com/Community-Access/git-going-with-github/issues/new?template=classroom-enrollment.yml | - -## Full Docs - -- **Architecture & how it works**: [GITHUB_CLASSROOM_ARCHITECTURE.md](./GITHUB_CLASSROOM_ARCHITECTURE.md) -- **Token security & setup**: [FINE_GRAINED_TOKEN_SETUP.md](./FINE_GRAINED_TOKEN_SETUP.md) -- **Complete checklist**: [ENROLLMENT_SETUP_CHECKLIST.md](./ENROLLMENT_SETUP_CHECKLIST.md) -- **Operation guide**: [CLASSROOM_INTEGRATION_GUIDE.md](./CLASSROOM_INTEGRATION_GUIDE.md) +|---|---| +| Day 1 link missing | Verify `CLASSROOM_DAY1_ASSIGNMENT_URL` is set with no extra spaces | +| Day 2 release did not trigger | Add `day1-complete` comment or apply `day2-eligible` label | +| Wrong assignment opened | Re-copy invite links and update variables | ---- +## Operational Notes -**Estimated time**: 30 minutes -**Prerequisite knowledge**: None (step-by-step instructions provided) -**Support**: Open issue in [Community-Access/support](https://github.com/Community-Access/support/issues) +- Student org membership is not part of this flow. +- Assignment acceptance remains student-driven in GitHub Classroom. +- Private intake and dashboard workflows continue to run independently. diff --git a/admin/REGISTRATION-QUICKSTART.md b/admin/REGISTRATION-QUICKSTART.md index 75e047c..3f27f6d 100644 --- a/admin/REGISTRATION-QUICKSTART.md +++ b/admin/REGISTRATION-QUICKSTART.md @@ -1,36 +1,25 @@ # Registration Automation Quickstart (Facilitator) -Use this when you need to enable registration plus classroom API automation quickly. +Use this when you need to enable registration with assignment-link automation quickly. For full background and troubleshooting, use [REGISTRATION-ADMIN.md](REGISTRATION-ADMIN.md). ## 5-Minute Setup -1. Create an admin token from a facilitator account with organization invitation permissions. -2. In repository settings, add secret: - - `CLASSROOM_ORG_ADMIN_TOKEN` -3. In repository settings, add variables: - - `CLASSROOM_ORG` +1. In repository settings, add variables: - `CLASSROOM_DAY1_ASSIGNMENT_URL` - `CLASSROOM_DAY2_ASSIGNMENT_URL` -4. Save all values. -5. Submit one test registration issue from a non-member test account. +2. Save all values. +3. Submit one test registration issue from a test account. ## Copy/Paste Settings Template Use this checklist while entering repository settings values. -### Repository Secret - -| Name | Value to paste | -|---|---| -| CLASSROOM_ORG_ADMIN_TOKEN | PASTE_ADMIN_TOKEN_HERE | - ### Repository Variables | Name | Value to paste | |---|---| -| CLASSROOM_ORG | Community-Access-Classroom | | CLASSROOM_DAY1_ASSIGNMENT_URL | https://classroom.github.com/a/REPLACE_DAY1_ID | | CLASSROOM_DAY2_ASSIGNMENT_URL | https://classroom.github.com/a/REPLACE_DAY2_ID | @@ -38,30 +27,28 @@ Use this checklist while entering repository settings values. - Confirm there are no extra spaces before or after values. - Confirm both assignment URLs open correctly in a logged-in browser. -- Confirm the org name matches exactly, including capitalization. ## Expected Result (Happy Path) After the test issue is opened and capacity is available: 1. Registration confirmation comment is posted. -2. Organization invite is sent (or detected as already pending/member). -3. Day 1 and Day 2 assignment links appear in the confirmation comment. -4. `registration` label is applied. +2. Day 1 and Day 2 assignment links appear in the confirmation comment. +3. `registration` label is applied. ## Fast Verification Checklist -- [ ] Test user received or already had organization invite - [ ] Confirmation comment includes assignment links - [ ] Duplicate submission closes automatically with duplicate message - [ ] Waitlist behavior still works when capacity is full +- [ ] Student can reply `ack`, then `day1-complete`, and receive Day 2 release comment ## Rollback (Immediate) -If anything behaves unexpectedly, disable classroom API automation without stopping registration: +If anything behaves unexpectedly, disable assignment-link posting without stopping registration: -1. Remove repository secret `CLASSROOM_ORG_ADMIN_TOKEN`, or -2. Clear repository variable `CLASSROOM_ORG` +1. Clear `CLASSROOM_DAY1_ASSIGNMENT_URL`, or +2. Clear `CLASSROOM_DAY2_ASSIGNMENT_URL` The registration workflow will continue standard confirmation, capacity checks, and CSV export. @@ -78,7 +65,7 @@ scripts/classroom/Reset-SupportHubEnvironment.ps1 1. Keep [REGISTRATION-ADMIN.md](REGISTRATION-ADMIN.md) open. 2. Watch Actions runs for `registration.yml` after each new registration. 3. Spot-check one confirmation comment every few runs. -4. If failures appear, use rollback and continue manual classroom invite flow. +4. If failures appear, use rollback and continue manual assignment link sharing. ## Privacy Reminder diff --git a/admin/qa-bundle/.github/workflows/registration.yml b/admin/qa-bundle/.github/workflows/registration.yml index 6536b38..dce6a19 100644 --- a/admin/qa-bundle/.github/workflows/registration.yml +++ b/admin/qa-bundle/.github/workflows/registration.yml @@ -142,84 +142,21 @@ jobs: labels: ['waitlist'] }); - - name: Invite registrant to classroom organization (optional) - id: classroom-invite - if: >- - steps.dup-check.outputs.is_duplicate == 'false' && - steps.capacity-check.outputs.is_full == 'false' && - vars.CLASSROOM_ORG != '' - uses: actions/github-script@v7 - env: - CLASSROOM_ORG: ${{ vars.CLASSROOM_ORG }} - with: - github-token: ${{ secrets.CLASSROOM_ORG_ADMIN_TOKEN }} - script: | - const org = process.env.CLASSROOM_ORG; - const username = context.payload.issue.user.login; - const userId = context.payload.issue.user.id; - let status = 'skipped'; - - try { - await github.rest.orgs.checkMembershipForUser({ org, username }); - status = 'already-member'; - core.info(`${username} is already a member of ${org}`); - } catch (error) { - if (error.status !== 404) { - throw error; - } - - const invites = await github.paginate(github.rest.orgs.listPendingInvitations, { - org, - per_page: 100 - }); - - const hasPendingInvite = invites.some(invite => - (invite.login || '').toLowerCase() === username.toLowerCase() - ); - - if (hasPendingInvite) { - status = 'already-invited'; - core.info(`${username} already has a pending invite to ${org}`); - } else { - await github.rest.orgs.createInvitation({ - org, - invitee_id: userId, - role: 'direct_member' - }); - status = 'invited'; - core.info(`Sent org invite for ${username} to ${org}`); - } - } - - core.setOutput('status', status); - - name: Post welcome comment if: steps.dup-check.outputs.is_duplicate == 'false' && steps.capacity-check.outputs.is_full == 'false' uses: actions/github-script@v7 env: - CLASSROOM_ORG: ${{ vars.CLASSROOM_ORG }} DAY1_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY1_ASSIGNMENT_URL }} DAY2_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY2_ASSIGNMENT_URL }} - CLASSROOM_INVITE_STATUS: ${{ steps.classroom-invite.outputs.status }} with: script: | - const org = process.env.CLASSROOM_ORG; const day1 = (process.env.DAY1_ASSIGNMENT_URL || '').trim(); const day2 = (process.env.DAY2_ASSIGNMENT_URL || '').trim(); - const inviteStatus = (process.env.CLASSROOM_INVITE_STATUS || '').trim(); let classroomSection = ''; - if (day1 || day2 || inviteStatus) { + if (day1 || day2) { const lines = ['## Classroom Access']; - if (inviteStatus === 'invited') { - lines.push(`- We sent an organization invitation for \`${org}\`. Please accept it from your GitHub notifications or email.`); - } else if (inviteStatus === 'already-invited') { - lines.push(`- You already have a pending invitation for \`${org}\`. Please accept it before using assignment links.`); - } else if (inviteStatus === 'already-member') { - lines.push(`- You are already a member of \`${org}\`.`); - } - if (day1) { lines.push(`- Day 1 assignment link: ${day1}`); } diff --git a/scripts/classroom/Initialize-WorkshopSetup.ps1 b/scripts/classroom/Initialize-WorkshopSetup.ps1 index 396f8c6..ba85851 100644 --- a/scripts/classroom/Initialize-WorkshopSetup.ps1 +++ b/scripts/classroom/Initialize-WorkshopSetup.ps1 @@ -4,27 +4,17 @@ .DESCRIPTION Runs every setup action that does not require a browser: - - Sets CLASSROOM_ORG_ADMIN_TOKEN repository secret - - Sets CLASSROOM_ORG, CLASSROOM_DAY1_ASSIGNMENT_URL, CLASSROOM_DAY2_ASSIGNMENT_URL variables + - Sets CLASSROOM_DAY1_ASSIGNMENT_URL and CLASSROOM_DAY2_ASSIGNMENT_URL variables - Verifies required labels exist (registration, duplicate, waitlist) - Syncs learning-room source into Community-Access/learning-room-template - Validates the template with a smoke repository - Optionally seeds challenges for the test student account - One step MUST be completed manually before running this script: - 1. Generate a classic PAT at https://github.com/settings/tokens with the admin:org scope. - Pass the token value to -AdminPAT. - Optionally, create Day 1 and Day 2 assignments at https://classroom.github.com before running this script. If the assignments already exist, the script will resolve their invite URLs automatically from the GitHub Classroom API and no URL parameters are needed. If assignments do not exist yet, the script will tell you what to create and exit cleanly. -.PARAMETER AdminPAT - The classic GitHub personal access token with admin:org scope generated by accesswatch. - This will be stored as the CLASSROOM_ORG_ADMIN_TOKEN repository secret. - Do not commit this value anywhere. - .PARAMETER Day1AssignmentUrl Optional. The GitHub Classroom invite URL for the Day 1 assignment (You Belong Here). Format: https://classroom.github.com/a/ @@ -60,28 +50,22 @@ .EXAMPLE # Minimal -- assignment URLs resolved automatically from Classroom API - .\Initialize-WorkshopSetup.ps1 -AdminPAT ghp_yourTokenHere + .\Initialize-WorkshopSetup.ps1 .EXAMPLE # Provide URLs explicitly (e.g. if auto-resolution title matching fails) .\Initialize-WorkshopSetup.ps1 ` - -AdminPAT ghp_yourTokenHere ` -Day1AssignmentUrl https://classroom.github.com/a/abc123 ` -Day2AssignmentUrl https://classroom.github.com/a/def456 .EXAMPLE # Skip template steps (already up to date) .\Initialize-WorkshopSetup.ps1 ` - -AdminPAT ghp_yourTokenHere ` -SkipTemplatePrepare -SkipTemplateValidate #> [CmdletBinding(SupportsShouldProcess)] param( - [Parameter(Mandatory = $true)] - [ValidatePattern('^ghp_[A-Za-z0-9]+$')] - [string]$AdminPAT, - [ValidatePattern('^https://classroom\.github\.com/a/[A-Za-z0-9_-]+$')] [string]$Day1AssignmentUrl, @@ -211,23 +195,12 @@ if (-not $Day1AssignmentUrl -or -not $Day2AssignmentUrl) { } -# --------------------------------------------------------------------------- -Write-Step "Setting CLASSROOM_ORG_ADMIN_TOKEN secret on $Repo" -$AdminPAT | gh secret set CLASSROOM_ORG_ADMIN_TOKEN -R $Repo --body - -if ($LASTEXITCODE -eq 0) { - Write-Pass "CLASSROOM_ORG_ADMIN_TOKEN set" -} else { - Write-Fail "Failed to set CLASSROOM_ORG_ADMIN_TOKEN" - $failures.Add("CLASSROOM_ORG_ADMIN_TOKEN secret") -} - # --------------------------------------------------------------------------- # Step 2: Set repository variables # --------------------------------------------------------------------------- Write-Step "Setting repository variables on $Repo" $variablesToSet = @( - @{ Name = 'CLASSROOM_ORG'; Value = $ClassroomOrg }, @{ Name = 'CLASSROOM_DAY1_ASSIGNMENT_URL'; Value = $Day1AssignmentUrl }, @{ Name = 'CLASSROOM_DAY2_ASSIGNMENT_URL'; Value = $Day2AssignmentUrl } )