Skip to content

Update 2FA documentation: GitHub now requires 2FA for all accounts #155

Update 2FA documentation: GitHub now requires 2FA for all accounts

Update 2FA documentation: GitHub now requires 2FA for all accounts #155

Workflow file for this run

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