From 89e77dbd0a28005fb5b0606d891e7c4ac898fa5c Mon Sep 17 00:00:00 2001 From: GitHub Workshop Bot Date: Thu, 2 Jul 2026 12:00:44 -0700 Subject: [PATCH] fix(provisioning): auto-discover App installation, alert on failure, close registration-to-roster gap Root cause of a month of failed runs: the provisioning GitHub App was never installed on the org, and the 30-minute schedule failed silently 280 times while two enrolled learners waited unprovisioned. - github-app-auth: normalize installation IDs (accept settings URLs) and discover the installation from the App when the secret is unset, with actionable 404 guidance - provision-cli: --check-auth credentials canary; template-repo preflight before the provisioning loop - new sync-intake-to-roster: [ENROLL-INTAKE] issues in the admin repo flow into roster.json automatically (idempotent, never touches existing entries) - provision-learning-rooms workflow: intake sync step, work-free early exit, roster commit on failure too, watchdog provisioning-alert issue opened on failure and closed on success, repository_dispatch trigger, healing-sweep cron instead of 30-minute polling, actions-bot spike mode removed - new provisioning-health-check workflow: weekly token mint + template access check with the same alerting - registration workflow: dispatches provisioning on enrollment, drops dead GitHub Classroom assignment links, stores [REGISTER] intake privately and redacts PII from public issue bodies - docs: SPEC 7.2a, OWNED_PROVISIONING, HYBRID_DEPLOYMENT_GUIDE updated; DEPLOYMENT_ASSESSMENT de-emojied with a July 2026 correction; full incident review in REVIEW-2026-07-02.md - hygiene: scratch tmp-* files and fix.md/fix.html removed and gitignored; authoritative-sources gate green at HEAD; HTML regenerated Co-Authored-By: Claude Fable 5 --- .../registration-workflow-readiness.test.js | 16 +- .../__tests__/github-app-auth.test.js | 130 +++- .../__tests__/provision-cli.test.js | 69 +++ .../__tests__/sync-intake-to-roster.test.js | 104 ++++ .../scripts/provisioning/github-app-auth.js | 94 ++- .github/scripts/provisioning/provision-cli.js | 72 ++- .../provisioning/sync-intake-to-roster.js | 176 ++++++ .../workflows/provision-learning-rooms.yml | 141 ++++- .../workflows/provisioning-health-check.yml | 91 +++ .github/workflows/registration.yml | 158 +++-- .gitignore | 4 + CLAUDE.md | 50 ++ DEPLOYMENT_ASSESSMENT.md | 68 ++- REVIEW-2026-07-02.md | 130 ++++ SPEC.md | 5 +- admin/HYBRID_DEPLOYMENT_GUIDE.md | 23 +- admin/OWNED_PROVISIONING.md | 23 +- fix.html | 237 -------- fix.md | 144 ----- html/CLAUDE.html | 286 +++++++++ html/DEPLOYMENT_ASSESSMENT.html | 557 ++++++++++++++++++ html/REVIEW-2026-07-02.html | 357 +++++++++++ html/SPEC.html | 5 +- html/admin/HYBRID_DEPLOYMENT_GUIDE.html | 26 +- html/admin/OWNED_PROVISIONING.html | 23 +- html/search-index.json | 54 +- scripts/validate-authoritative-sources.js | 4 + tmp-audio-inventory.csv | 80 --- tmp-crosslink-rewrite-report.txt | 17 - tmp-learning-room-smoke-test.ps1 | 370 ------------ tmp-learning-room-smoke-wrapper.ps1 | 33 -- tmp-proposed-feed-order.csv | 80 --- tmp-proposed-rename.csv | 59 -- tmp-proposed-stage-1-1.csv | 80 --- tmp-rename-audit.log | 119 ---- 35 files changed, 2532 insertions(+), 1353 deletions(-) create mode 100644 .github/scripts/provisioning/__tests__/sync-intake-to-roster.test.js create mode 100644 .github/scripts/provisioning/sync-intake-to-roster.js create mode 100644 .github/workflows/provisioning-health-check.yml create mode 100644 CLAUDE.md create mode 100644 REVIEW-2026-07-02.md delete mode 100644 fix.html delete mode 100644 fix.md create mode 100644 html/CLAUDE.html create mode 100644 html/DEPLOYMENT_ASSESSMENT.html create mode 100644 html/REVIEW-2026-07-02.html delete mode 100644 tmp-audio-inventory.csv delete mode 100644 tmp-crosslink-rewrite-report.txt delete mode 100644 tmp-learning-room-smoke-test.ps1 delete mode 100644 tmp-learning-room-smoke-wrapper.ps1 delete mode 100644 tmp-proposed-feed-order.csv delete mode 100644 tmp-proposed-rename.csv delete mode 100644 tmp-proposed-stage-1-1.csv delete mode 100644 tmp-rename-audit.log diff --git a/.github/scripts/__tests__/registration-workflow-readiness.test.js b/.github/scripts/__tests__/registration-workflow-readiness.test.js index 854ed145..badc4ca7 100644 --- a/.github/scripts/__tests__/registration-workflow-readiness.test.js +++ b/.github/scripts/__tests__/registration-workflow-readiness.test.js @@ -17,11 +17,13 @@ test('registration workflow contains required label and duplicate/waitlist flows "labels: ['duplicate']", "labels: ['waitlist']", "labels: ['registration']", - 'CLASSROOM_DAY1_ASSIGNMENT_URL', - 'CLASSROOM_DAY2_ASSIGNMENT_URL', - 'Please reply `ack` on this issue after you confirm your Day 1 link works.', + 'Please reply `ack` on this issue after you receive your learning room invitation.', 'Upload CSV as artifact', 'Sync Student Roster (No PII)', + // Hybrid model: enrollment triggers provisioning directly. + 'createWorkflowDispatch', + // PII hygiene: the [REGISTER] path stores intake privately, then redacts. + 'Registration submission (redacted)', ]; requiredSnippets.forEach(snippet => { @@ -31,6 +33,14 @@ test('registration workflow contains required label and duplicate/waitlist flows `Registration workflow missing expected behavior marker: ${snippet}` ); }); + + // GitHub Classroom was removed in the June 2026 Hybrid transition; the + // workflow must never send registrants to classroom.github.com again. + assert.equal( + /CLASSROOM_DAY[12]_ASSIGNMENT_URL|classroom\.github\.com/i.test(workflow), + false, + 'Registration workflow must not reference removed GitHub Classroom assignment links' + ); }); test('registration issue form template exists', () => { diff --git a/.github/scripts/provisioning/__tests__/github-app-auth.test.js b/.github/scripts/provisioning/__tests__/github-app-auth.test.js index 831a7423..77db2a9c 100644 --- a/.github/scripts/provisioning/__tests__/github-app-auth.test.js +++ b/.github/scripts/provisioning/__tests__/github-app-auth.test.js @@ -2,7 +2,13 @@ const test = require('node:test'); const assert = require('node:assert/strict'); const crypto = require('node:crypto'); -const { createAppJwt, mintInstallationToken, base64url } = require('../github-app-auth'); +const { + createAppJwt, + mintInstallationToken, + base64url, + normalizeInstallationId, + discoverInstallationId +} = require('../github-app-auth'); const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048, @@ -69,6 +75,29 @@ test('mintInstallationToken posts to the right endpoint and returns the token', assert.match(captured.opts.headers.Authorization, /^Bearer /); }); +test('mintInstallationToken accepts installation settings URLs', async () => { + let captured; + const fakeFetch = async (url) => { + captured = url; + return { + status: 201, + async json() { + return { token: 'ghs_faketoken', expires_at: '2026-01-01T01:00:00Z' }; + } + }; + }; + + await mintInstallationToken({ + appId: '12345', + privateKey, + installationId: + 'https://github.com/organizations/Community-Access/settings/installations/99?target_type=Organization', + fetchImpl: fakeFetch + }); + + assert.match(captured, /\/app\/installations\/99\/access_tokens$/); +}); + test('mintInstallationToken throws a clear error on failure', async () => { const fakeFetch = async () => ({ status: 403, @@ -78,6 +107,103 @@ test('mintInstallationToken throws a clear error on failure', async () => { }); await assert.rejects( () => mintInstallationToken({ appId: '1', privateKey, installationId: '2', fetchImpl: fakeFetch }), - /HTTP 403.*forbidden/ + /HTTP 403.*forbidden\./ + ); +}); + +test('normalizeInstallationId accepts numeric IDs and installation URLs', () => { + assert.equal(normalizeInstallationId('12345'), '12345'); + assert.equal( + normalizeInstallationId( + 'https://github.com/organizations/Community-Access/settings/installations/98765432' + ), + '98765432' + ); +}); + +test('normalizeInstallationId rejects invalid values', () => { + assert.throws(() => normalizeInstallationId(''), /requires installationId/); + assert.throws(() => normalizeInstallationId('not-an-installation-id'), /Invalid installationId/); +}); + +test('discoverInstallationId finds the installation for the target owner', async () => { + let captured; + const fakeFetch = async (url, opts) => { + captured = { url, opts }; + return { + status: 200, + async json() { + return [ + { id: 111, account: { login: 'Someone-Else' } }, + { id: 222, account: { login: 'Community-Access' } } + ]; + } + }; + }; + const id = await discoverInstallationId({ + appId: '12345', + privateKey, + owner: 'community-access', + fetchImpl: fakeFetch + }); + assert.equal(id, '222'); + assert.match(captured.url, /\/app\/installations\?per_page=100$/); + assert.match(captured.opts.headers.Authorization, /^Bearer /); +}); + +test('discoverInstallationId explains when the app is not installed on the owner', async () => { + const fakeFetch = async () => ({ + status: 200, + async json() { + return [{ id: 111, account: { login: 'Someone-Else' } }]; + } + }); + await assert.rejects( + () => + discoverInstallationId({ + appId: '12345', + privateKey, + owner: 'Community-Access', + fetchImpl: fakeFetch + }), + /not installed on Community-Access/ + ); +}); + +test('discoverInstallationId explains when the app has no installations at all', async () => { + const fakeFetch = async () => ({ + status: 200, + async json() { + return []; + } + }); + await assert.rejects( + () => + discoverInstallationId({ + appId: '12345', + privateKey, + owner: 'Community-Access', + fetchImpl: fakeFetch + }), + /no installations.*Install the App/i + ); +}); + +test('discoverInstallationId surfaces API errors', async () => { + const fakeFetch = async () => ({ + status: 401, + async text() { + return 'bad credentials'; + } + }); + await assert.rejects( + () => + discoverInstallationId({ + appId: '12345', + privateKey, + owner: 'Community-Access', + fetchImpl: fakeFetch + }), + /HTTP 401.*bad credentials/ ); }); diff --git a/.github/scripts/provisioning/__tests__/provision-cli.test.js b/.github/scripts/provisioning/__tests__/provision-cli.test.js index 8b856ad9..33d0fb10 100644 --- a/.github/scripts/provisioning/__tests__/provision-cli.test.js +++ b/.github/scripts/provisioning/__tests__/provision-cli.test.js @@ -14,6 +14,11 @@ test('parseArgs reads roster, log, dry-run, help', () => { assert.equal(parseArgs(['-h']).help, true); }); +test('parseArgs reads check-auth', () => { + assert.equal(parseArgs(['--check-auth']).checkAuth, true); + assert.equal(parseArgs(['--roster', 'r.json']).checkAuth, false); +}); + test('usage mentions both modes', () => { assert.match(usage(), /github-app/); assert.match(usage(), /actions-bot/); @@ -54,3 +59,67 @@ test('resolveToken rejects unknown mode', async () => { /Unknown PROVISIONING_MODE/ ); }); + +const crypto = require('node:crypto'); +const { privateKey: testPem } = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' } +}); + +function fakeAppApi() { + const calls = []; + const fetchImpl = async (url, opts) => { + calls.push({ url, opts }); + if (/\/app\/installations\?/.test(url)) { + return { + status: 200, + async json() { + return [{ id: 777, account: { login: 'Community-Access' } }]; + } + }; + } + if (/\/app\/installations\/777\/access_tokens$/.test(url)) { + return { + status: 201, + async json() { + return { token: 'ghs_discovered', expires_at: '2026-01-01T01:00:00Z' }; + } + }; + } + return { status: 404, async text() { return 'unexpected call'; } }; + }; + return { calls, fetchImpl }; +} + +test('resolveToken github-app mode discovers the installation when the ID is unset', async () => { + const { fetchImpl } = fakeAppApi(); + const token = await resolveToken( + { + PROVISIONING_MODE: 'github-app', + PROVISIONING_APP_ID: '12345', + PROVISIONING_APP_PRIVATE_KEY: testPem, + PROVISIONING_STUDENT_OWNER: 'Community-Access' + }, + 'https://api.github.com', + { fetchImpl } + ); + assert.equal(token, 'ghs_discovered'); +}); + +test('resolveToken github-app mode uses a configured installation ID directly', async () => { + const { calls, fetchImpl } = fakeAppApi(); + const token = await resolveToken( + { + PROVISIONING_MODE: 'github-app', + PROVISIONING_APP_ID: '12345', + PROVISIONING_APP_PRIVATE_KEY: testPem, + PROVISIONING_APP_INSTALLATION_ID: '777', + PROVISIONING_STUDENT_OWNER: 'Community-Access' + }, + 'https://api.github.com', + { fetchImpl } + ); + assert.equal(token, 'ghs_discovered'); + assert.equal(calls.some((c) => /\/app\/installations\?/.test(c.url)), false); +}); diff --git a/.github/scripts/provisioning/__tests__/sync-intake-to-roster.test.js b/.github/scripts/provisioning/__tests__/sync-intake-to-roster.test.js new file mode 100644 index 00000000..341a46a9 --- /dev/null +++ b/.github/scripts/provisioning/__tests__/sync-intake-to-roster.test.js @@ -0,0 +1,104 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { + parseIntakeIssue, + syncIntakeToRoster +} = require('../sync-intake-to-roster'); + +function intakeIssue({ number = 1, login = 'octocat', captured = '2026-06-05T12:00:00.000Z', title } = {}) { + return { + number, + title: title || `[ENROLL-INTAKE] Test Person ${number}`, + created_at: '2026-06-05T11:59:00Z', + body: [ + '## Classroom Enrollment Intake', + '', + '- Public issue: https://github.com/Community-Access/git-going-with-github/issues/42', + `- Submitted by: @${login}`, + '- Full Name: Test Person', + '- Email Address: test@example.com', + '', + `- Captured at: ${captured}` + ].join('\n') + }; +} + +test('parseIntakeIssue extracts handle and registration time', () => { + const parsed = parseIntakeIssue(intakeIssue({ login: 'Some-User' })); + assert.equal(parsed.github_handle, 'Some-User'); + assert.equal(parsed.registered_at, '2026-06-05T12:00:00.000Z'); +}); + +test('parseIntakeIssue falls back to issue created_at and rejects non-intake issues', () => { + const noCapture = intakeIssue(); + noCapture.body = noCapture.body.replace(/- Captured at: .*/, ''); + assert.equal(parseIntakeIssue(noCapture).registered_at, '2026-06-05T11:59:00Z'); + + assert.equal(parseIntakeIssue({ title: 'Some other issue', body: '' }), null); + const badHandle = intakeIssue(); + badHandle.body = badHandle.body.replace(/@octocat/, '@-bad-handle-'); + assert.equal(parseIntakeIssue(badHandle), null); +}); + +test('syncIntakeToRoster adds pending learners and the cohort, idempotently', () => { + const roster = { version: 1, cohorts: [], learners: [] }; + const issues = [ + intakeIssue({ number: 1, login: 'alice' }), + intakeIssue({ number: 2, login: 'bob' }), + intakeIssue({ number: 3, login: 'alice' }) // duplicate submission + ]; + const result = syncIntakeToRoster({ roster, intakeIssues: issues, cohortId: 'self-paced-2026' }); + + assert.deepEqual(result.added.sort(), ['alice', 'bob']); + assert.equal(result.roster.learners.length, 2); + assert.ok(result.roster.learners.every((l) => l.provision_state === 'pending')); + assert.ok(result.roster.cohorts.some((c) => c.cohort_id === 'self-paced-2026')); + + // Second run adds nothing. + const again = syncIntakeToRoster({ + roster: result.roster, + intakeIssues: issues, + cohortId: 'self-paced-2026' + }); + assert.deepEqual(again.added, []); + assert.equal(again.roster.learners.length, 2); +}); + +test('syncIntakeToRoster never resets an already-provisioned learner', () => { + const roster = { + version: 1, + cohorts: [{ cohort_id: 'self-paced-2026', label: 'Self-paced' }], + learners: [ + { + github_handle: 'alice', + cohort_id: 'self-paced-2026', + path: 'day1-day2', + learning_room_repo: 'Community-Access/learning-room-self-paced-2026-alice', + provision_state: 'provisioned', + status: 'active-day1', + registered_at: '2026-06-01T00:00:00Z', + last_signal_at: null, + notes: '' + } + ] + }; + const result = syncIntakeToRoster({ + roster, + intakeIssues: [intakeIssue({ login: 'alice' })], + cohortId: 'self-paced-2026' + }); + assert.deepEqual(result.added, []); + assert.equal(result.roster.learners[0].provision_state, 'provisioned'); + assert.equal( + result.roster.learners[0].learning_room_repo, + 'Community-Access/learning-room-self-paced-2026-alice' + ); +}); + +test('syncIntakeToRoster requires a cohort id', () => { + assert.throws( + () => syncIntakeToRoster({ roster: { version: 1, cohorts: [], learners: [] }, intakeIssues: [] }), + /cohortId/ + ); +}); diff --git a/.github/scripts/provisioning/github-app-auth.js b/.github/scripts/provisioning/github-app-auth.js index 3f501cc5..e49d97cc 100644 --- a/.github/scripts/provisioning/github-app-auth.js +++ b/.github/scripts/provisioning/github-app-auth.js @@ -62,10 +62,11 @@ async function mintInstallationToken({ fetchImpl = fetch, now = Date.now() }) { - if (!installationId) throw new Error('mintInstallationToken requires installationId'); + const normalizedInstallationId = normalizeInstallationId(installationId); const jwt = createAppJwt({ appId, privateKey, now }); + const baseUrl = String(apiBaseUrl || 'https://api.github.com').replace(/\/+$/, ''); const res = await fetchImpl( - `${apiBaseUrl}/app/installations/${installationId}/access_tokens`, + `${baseUrl}/app/installations/${normalizedInstallationId}/access_tokens`, { method: 'POST', headers: { @@ -78,14 +79,93 @@ async function mintInstallationToken({ ); if (res.status !== 201) { const detail = await safeText(res); + const hint = + res.status === 404 + ? ' Check PROVISIONING_APP_INSTALLATION_ID (numeric ID or the installation settings URL),' + + ' and confirm the App is actually installed on the target organization;' + + ' leaving PROVISIONING_APP_INSTALLATION_ID unset lets provisioning discover it automatically.' + : ''; throw new Error( - `Failed to mint installation token (HTTP ${res.status}): ${detail}` + `Failed to mint installation token (HTTP ${res.status}): ${detail}.${hint}` ); } const body = await res.json(); return { token: body.token, expires_at: body.expires_at }; } +/** + * Discover the App's installation ID for a target org/user by listing the App's + * installations with an App JWT. Removes the need to store the installation ID + * as a secret: it cannot be mis-pasted, and it survives App re-installation + * (which changes the ID). + */ +async function discoverInstallationId({ + appId, + privateKey, + owner, + apiBaseUrl = 'https://api.github.com', + fetchImpl = fetch, + now = Date.now() +}) { + if (!owner) throw new Error('discoverInstallationId requires owner'); + const jwt = createAppJwt({ appId, privateKey, now }); + const baseUrl = String(apiBaseUrl || 'https://api.github.com').replace(/\/+$/, ''); + const res = await fetchImpl(`${baseUrl}/app/installations?per_page=100`, { + method: 'GET', + headers: { + Authorization: `Bearer ${jwt}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'glow-provisioning' + } + }); + if (res.status !== 200) { + const detail = await safeText(res); + throw new Error(`Failed to list App installations (HTTP ${res.status}): ${detail}`); + } + const installations = await res.json(); + if (!Array.isArray(installations) || installations.length === 0) { + throw new Error( + 'The GitHub App has no installations. Install the App on the target organization ' + + '(Organization settings -> GitHub Apps) before provisioning.' + ); + } + const wanted = String(owner).toLowerCase(); + const match = installations.find( + (i) => i && i.account && String(i.account.login).toLowerCase() === wanted + ); + if (!match) { + const installed = installations + .map((i) => (i && i.account ? i.account.login : 'unknown')) + .join(', '); + throw new Error( + `The GitHub App is not installed on ${owner}. It is installed on: ${installed}. ` + + 'Install the App on the target organization before provisioning.' + ); + } + return String(match.id); +} + +function normalizeInstallationId(value) { + if (value == null) { + throw new Error('mintInstallationToken requires installationId'); + } + const raw = String(value).trim(); + if (!raw) { + throw new Error('mintInstallationToken requires installationId'); + } + if (/^\d+$/.test(raw)) { + return raw; + } + const urlMatch = raw.match(/\/installations\/(\d+)(?:[/?#]|$)/i); + if (urlMatch) { + return urlMatch[1]; + } + throw new Error( + 'Invalid installationId. Provide the numeric ID or a full URL that includes /installations/.' + ); +} + async function safeText(res) { try { return await res.text(); @@ -94,4 +174,10 @@ async function safeText(res) { } } -module.exports = { createAppJwt, mintInstallationToken, base64url }; +module.exports = { + createAppJwt, + mintInstallationToken, + base64url, + normalizeInstallationId, + discoverInstallationId +}; diff --git a/.github/scripts/provisioning/provision-cli.js b/.github/scripts/provisioning/provision-cli.js index e66f875d..daea5fa2 100644 --- a/.github/scripts/provisioning/provision-cli.js +++ b/.github/scripts/provisioning/provision-cli.js @@ -26,15 +26,16 @@ const path = require('node:path'); const { parseRoster, serializeRoster } = require('./roster'); const { provisionRoster } = require('./provision-core'); const { createFetchClient, REQUIRED_WORKFLOWS } = require('./github-client'); -const { mintInstallationToken } = require('./github-app-auth'); +const { mintInstallationToken, discoverInstallationId } = require('./github-app-auth'); function parseArgs(argv) { - const args = { dryRun: false }; + const args = { dryRun: false, checkAuth: false }; for (let i = 0; i < argv.length; i += 1) { const a = argv[i]; if (a === '--roster') args.roster = argv[++i]; else if (a === '--log') args.log = argv[++i]; else if (a === '--dry-run') args.dryRun = true; + else if (a === '--check-auth') args.checkAuth = true; else if (a === '--help' || a === '-h') args.help = true; } return args; @@ -43,6 +44,10 @@ function parseArgs(argv) { function usage() { return [ 'Usage: node provision-cli.js --roster [--log ] [--dry-run]', + ' node provision-cli.js --check-auth', + '', + '--check-auth mints a token and verifies the template repo is reachable,', + 'then exits. Use it as a credentials health check; it makes no changes.', '', 'Environment:', ' PROVISIONING_MODE github-app | actions-bot', @@ -52,7 +57,8 @@ function usage() { ' github-app mode:', ' PROVISIONING_APP_ID', ' PROVISIONING_APP_PRIVATE_KEY (PEM, or @path to a PEM file)', - ' PROVISIONING_APP_INSTALLATION_ID', + ' PROVISIONING_APP_INSTALLATION_ID (optional; discovered from the', + ' App installations when unset)', ' actions-bot mode:', ' PROVISIONING_TOKEN' ].join('\n'); @@ -69,18 +75,34 @@ function readPrivateKey(value) { : value; } -async function resolveToken(env, apiBaseUrl) { +async function resolveToken(env, apiBaseUrl, deps = {}) { const mode = env.PROVISIONING_MODE || 'github-app'; if (mode === 'actions-bot') { if (!env.PROVISIONING_TOKEN) throw new Error('actions-bot mode requires PROVISIONING_TOKEN'); return env.PROVISIONING_TOKEN; } if (mode === 'github-app') { + const appId = env.PROVISIONING_APP_ID; + const privateKey = readPrivateKey(env.PROVISIONING_APP_PRIVATE_KEY); + const fetchImpl = deps.fetchImpl || fetch; + let installationId = String(env.PROVISIONING_APP_INSTALLATION_ID || '').trim(); + if (!installationId) { + // No stored ID: discover it from the App's installations. This removes a + // copy-paste failure mode and survives App re-installation. + installationId = await discoverInstallationId({ + appId, + privateKey, + owner: env.PROVISIONING_STUDENT_OWNER, + apiBaseUrl, + fetchImpl + }); + } const { token } = await mintInstallationToken({ - appId: env.PROVISIONING_APP_ID, - privateKey: readPrivateKey(env.PROVISIONING_APP_PRIVATE_KEY), - installationId: env.PROVISIONING_APP_INSTALLATION_ID, - apiBaseUrl + appId, + privateKey, + installationId, + apiBaseUrl, + fetchImpl }); return token; } @@ -89,7 +111,7 @@ async function resolveToken(env, apiBaseUrl) { async function main() { const args = parseArgs(process.argv.slice(2)); - if (args.help || !args.roster) { + if (args.help || (!args.roster && !args.checkAuth)) { process.stdout.write(`${usage()}\n`); process.exit(args.help ? 0 : 2); } @@ -105,6 +127,25 @@ async function main() { throw new Error('LEARNING_ROOM_TEMPLATE_REPO must be owner/name'); } + if (args.checkAuth) { + const token = await resolveToken(env, apiBaseUrl); + const client = createFetchClient({ token, apiBaseUrl }); + const templateVisible = await client.repoExists({ + owner: templateOwner, + repo: templateRepo + }); + if (!templateVisible) { + throw new Error( + `Auth check failed: token minted but cannot see ${templateRepoFull}. ` + + 'Check the App installation repository access and Contents permission.' + ); + } + process.stdout.write( + `Auth check OK: token minted and template ${templateRepoFull} is reachable.\n` + ); + return; + } + const rosterJson = fs.readFileSync(args.roster, 'utf8'); const roster = parseRoster(rosterJson); @@ -122,6 +163,19 @@ async function main() { const token = await resolveToken(env, apiBaseUrl); const client = createFetchClient({ token, apiBaseUrl }); + // Preflight: fail with one clear message if the token cannot even see the + // template, instead of a confusing per-learner error cascade. + const templateVisible = await client.repoExists({ + owner: templateOwner, + repo: templateRepo + }); + if (!templateVisible) { + throw new Error( + `Preflight failed: cannot see template ${templateRepoFull} with the minted token. ` + + 'Check the App installation repository access and Contents permission.' + ); + } + const { roster: updated, log, summary } = await provisionRoster({ roster, client, diff --git a/.github/scripts/provisioning/sync-intake-to-roster.js b/.github/scripts/provisioning/sync-intake-to-roster.js new file mode 100644 index 00000000..57cb449b --- /dev/null +++ b/.github/scripts/provisioning/sync-intake-to-roster.js @@ -0,0 +1,176 @@ +#!/usr/bin/env node +/** + * Sync enrollment intake issues into the roster of record. + * + * The registration workflow stores each [ENROLL] submission as an + * "[ENROLL-INTAKE] ..." issue in the private admin repository. Until now a + * human had to hand-copy those into roster.json before provisioning could see + * them - the manual hop that left enrollees unprovisioned. This script closes + * that gap: it reads the open intake issues, upserts a `pending` roster entry + * for each new learner (idempotent on github_handle + cohort_id, and it never + * touches an existing entry), and writes the roster back for the provisioning + * loop to act on. + * + * Pure parsing/merge logic is exported for unit tests; only main() touches the + * network, using the same injected-fetch style as the rest of the subsystem. + * + * Usage: + * node sync-intake-to-roster.js --roster path/to/roster.json + * + * Environment: + * INTAKE_REPO owner/name of the private admin repo (required) + * INTAKE_TOKEN token that can read issues in INTAKE_REPO (required) + * PROVISIONING_COHORT_ID cohort to enroll new learners into (required) + * GITHUB_API_URL default https://api.github.com + */ + +'use strict'; + +const fs = require('node:fs'); + +const { parseRoster, serializeRoster, findLearner, upsertLearner, isValidHandle } = + require('./roster'); + +const INTAKE_TITLE_PREFIX = '[ENROLL-INTAKE]'; + +/** + * Extract { github_handle, registered_at } from an intake issue, or null when + * the issue is not a parseable intake record. + */ +function parseIntakeIssue(issue) { + if (!issue || typeof issue.title !== 'string') return null; + if (!issue.title.startsWith(INTAKE_TITLE_PREFIX)) return null; + const body = String(issue.body || ''); + const submitted = body.match(/^- Submitted by: @(\S+)\s*$/m); + if (!submitted || !isValidHandle(submitted[1])) return null; + const captured = body.match(/^- Captured at: (\S+)\s*$/m); + return { + github_handle: submitted[1], + registered_at: (captured && captured[1]) || issue.created_at || null, + intake_issue_number: issue.number + }; +} + +/** + * Merge intake issues into the roster. Returns { roster, added, skipped }. + * Existing learners (any provision_state) are never modified, so a re-run or a + * duplicate submission can never reset someone who is already provisioned. + */ +function syncIntakeToRoster({ roster, intakeIssues, cohortId }) { + if (!cohortId) throw new Error('syncIntakeToRoster requires cohortId'); + let next = { + version: 1, + generated_at: roster.generated_at, + cohorts: (roster.cohorts || []).map((c) => ({ ...c })), + learners: (roster.learners || []).map((l) => ({ ...l })) + }; + const added = []; + const skipped = []; + + for (const issue of intakeIssues || []) { + const parsed = parseIntakeIssue(issue); + if (!parsed) { + if (issue && issue.title && String(issue.title).startsWith(INTAKE_TITLE_PREFIX)) { + skipped.push({ number: issue.number, reason: 'unparseable intake issue' }); + } + continue; + } + if (findLearner(next, parsed.github_handle, cohortId)) { + continue; // already known (pending, provisioned, or otherwise) - leave untouched + } + next = upsertLearner(next, { + github_handle: parsed.github_handle, + cohort_id: cohortId, + path: 'day1-day2', + provision_state: 'pending', + status: 'awaiting-ack', + registered_at: parsed.registered_at, + notes: `from intake issue #${parsed.intake_issue_number}` + }); + added.push(parsed.github_handle); + } + + if (added.length && !next.cohorts.some((c) => c.cohort_id === cohortId)) { + next.cohorts.push({ cohort_id: cohortId, label: 'Self-paced enrollment' }); + } + + return { roster: next, added, skipped }; +} + +async function fetchOpenIntakeIssues({ repoFull, token, apiBaseUrl, fetchImpl = fetch }) { + const baseUrl = String(apiBaseUrl || 'https://api.github.com').replace(/\/+$/, ''); + const issues = []; + for (let page = 1; page <= 10; page += 1) { + const res = await fetchImpl( + `${baseUrl}/repos/${repoFull}/issues?state=open&per_page=100&page=${page}`, + { + headers: { + Authorization: `token ${token}`, + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'glow-provisioning' + } + } + ); + if (res.status !== 200) { + const detail = await res.text(); + throw new Error(`Failed to list intake issues (HTTP ${res.status}): ${detail}`); + } + const batch = await res.json(); + for (const issue of batch) { + if (!issue.pull_request) issues.push(issue); + } + if (batch.length < 100) break; + } + return issues; +} + +async function main() { + const rosterFlag = process.argv.indexOf('--roster'); + const rosterPath = rosterFlag >= 0 ? process.argv[rosterFlag + 1] : null; + if (!rosterPath) { + process.stderr.write('Usage: node sync-intake-to-roster.js --roster \n'); + process.exit(2); + } + const env = process.env; + const repoFull = env.INTAKE_REPO; + const token = env.INTAKE_TOKEN; + const cohortId = env.PROVISIONING_COHORT_ID; + if (!repoFull || !repoFull.includes('/')) throw new Error('INTAKE_REPO (owner/name) is required'); + if (!token) throw new Error('INTAKE_TOKEN is required'); + if (!cohortId) throw new Error('PROVISIONING_COHORT_ID is required'); + + const roster = parseRoster(fs.readFileSync(rosterPath, 'utf8')); + const intakeIssues = await fetchOpenIntakeIssues({ + repoFull, + token, + apiBaseUrl: env.GITHUB_API_URL + }); + + const { roster: updated, added, skipped } = syncIntakeToRoster({ + roster, + intakeIssues, + cohortId + }); + + if (added.length) { + fs.writeFileSync(rosterPath, serializeRoster(updated)); + } + process.stdout.write( + `Intake sync: ${added.length} new learner(s) added to roster` + + (added.length ? ` (${added.join(', ')})` : '') + + `; ${skipped.length} unparseable intake issue(s).\n` + ); + for (const s of skipped) { + process.stdout.write(` Warning: intake issue #${s.number}: ${s.reason}\n`); + } +} + +if (require.main === module) { + main().catch((err) => { + process.stderr.write(`Intake sync failed: ${err.message}\n`); + process.exit(1); + }); +} + +module.exports = { parseIntakeIssue, syncIntakeToRoster, fetchOpenIntakeIssues }; diff --git a/.github/workflows/provision-learning-rooms.yml b/.github/workflows/provision-learning-rooms.yml index ed3eb47c..edb72102 100644 --- a/.github/workflows/provision-learning-rooms.yml +++ b/.github/workflows/provision-learning-rooms.yml @@ -2,11 +2,19 @@ name: Provision Learning Rooms (owned, GitHub-native) # Owned, GitHub-native replacement for GitHub Classroom provisioning. # Implements SPEC.md section 7. The roster of record lives in a private admin -# repository; this workflow reads it, idempotently provisions every pending or -# failed learner, and commits the updated roster plus the provisioning log back. +# repository; this workflow syncs enrollment intake issues into the roster, +# idempotently provisions every pending or failed learner, and commits the +# updated roster plus the provisioning log back. # # Nothing here is destructive: re-running heals partial failures and never # corrupts an existing student repository (idempotent on github_handle+cohort). +# +# Triggers: +# - repository_dispatch (provision-learning-rooms): fired by the registration +# workflow when a new enrollment lands, so learners get rooms in minutes. +# - schedule: a healing sweep a few times per day that re-syncs intake and +# retries any failed entries. +# - workflow_dispatch: manual runs, with an optional dry-run. on: workflow_dispatch: @@ -15,16 +23,19 @@ on: description: List who would be provisioned without making changes type: boolean default: false + repository_dispatch: + types: [provision-learning-rooms] schedule: - # Trickle-provision newly registered learners every 30 minutes. Provisioning - # on registration (a trickle) avoids the burst that would trip rate limits. - - cron: '*/30 * * * *' + # Healing sweep. Event-driven provisioning (repository_dispatch above) + # covers the fast path, so the cron only needs to catch stragglers. + - cron: '17 */6 * * *' -# Least privilege: this workflow needs no write scope on THIS repository. All -# writes happen against the private admin repo and student repos via a scoped -# token or GitHub App installation token configured in secrets. +# Least privilege: contents read to run the scripts; issues write only for the +# watchdog alert issue in this repository. All student-repo writes happen via +# the GitHub App installation token configured in the provisioning environment. permissions: contents: read + issues: write concurrency: group: provision-learning-rooms @@ -51,6 +62,29 @@ jobs: token: ${{ secrets.PRIVATE_STUDENT_DATA_TOKEN }} path: _roster + - name: Sync enrollment intake issues into the roster + if: ${{ !inputs.dry_run }} + run: | + node .github/scripts/provisioning/sync-intake-to-roster.js \ + --roster _roster/roster.json + env: + INTAKE_REPO: ${{ vars.ADMIN_ROSTER_REPO }} + INTAKE_TOKEN: ${{ secrets.PRIVATE_STUDENT_DATA_TOKEN }} + PROVISIONING_COHORT_ID: ${{ vars.PROVISIONING_COHORT_ID }} + GITHUB_API_URL: ${{ github.api_url }} + + - name: Count pending work + id: pending + run: | + count=$(node -e " + const fs = require('node:fs'); + const { parseRoster, entriesToProvision } = require('./.github/scripts/provisioning/roster'); + const roster = parseRoster(fs.readFileSync('_roster/roster.json', 'utf8')); + console.log(entriesToProvision(roster).length); + ") + echo "count=$count" >> "$GITHUB_OUTPUT" + echo "$count learner(s) pending or failed." + - name: Provision (dry run) if: ${{ inputs.dry_run }} run: | @@ -61,7 +95,7 @@ jobs: PROVISIONING_STUDENT_OWNER: ${{ vars.PROVISIONING_STUDENT_OWNER }} - name: Provision learners - if: ${{ !inputs.dry_run }} + if: ${{ !inputs.dry_run && steps.pending.outputs.count != '0' }} run: | node .github/scripts/provisioning/provision-cli.js \ --roster _roster/roster.json \ @@ -71,15 +105,15 @@ jobs: LEARNING_ROOM_TEMPLATE_REPO: ${{ vars.LEARNING_ROOM_TEMPLATE_REPO }} PROVISIONING_STUDENT_OWNER: ${{ vars.PROVISIONING_STUDENT_OWNER }} GITHUB_API_URL: ${{ github.api_url }} - # github-app mode (production): PROVISIONING_APP_ID: ${{ secrets.PROVISIONING_APP_ID }} PROVISIONING_APP_PRIVATE_KEY: ${{ secrets.PROVISIONING_APP_PRIVATE_KEY }} + # Optional: discovered from the App installations when unset. PROVISIONING_APP_INSTALLATION_ID: ${{ secrets.PROVISIONING_APP_INSTALLATION_ID }} - # actions-bot mode (Phase 1 spike only): - PROVISIONING_TOKEN: ${{ secrets.PROVISIONING_TOKEN }} + # Runs even when provisioning failed: per-learner failed states and the + # provisioning log are exactly what a facilitator needs to see. - name: Commit updated roster and provisioning log - if: ${{ !inputs.dry_run }} + if: ${{ always() && !inputs.dry_run }} working-directory: _roster run: | if [ -n "$(git status --porcelain)" ]; then @@ -91,3 +125,84 @@ jobs: else echo "No roster changes to commit." fi + + # Watchdog (SPEC.md section 7.3): any failure must be visible to a human + # before a learner notices. Opens or updates a pinned alert issue. + - name: Open or update provisioning alert issue + if: ${{ failure() }} + uses: actions/github-script@v8 + with: + script: | + const label = 'provisioning-alert'; + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + color: 'b60205', + description: 'Automated alert: learning room provisioning is failing' + }); + } catch (e) { /* label already exists */ } + + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const open = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: label, + state: 'open', + per_page: 1 + }); + + if (open.data.length) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: open.data[0].number, + body: `Provisioning failed again: ${runUrl}` + }); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Learning room provisioning is failing', + labels: [label], + body: [ + '## Provisioning watchdog', + '', + 'The Provision Learning Rooms workflow failed. Learners may be waiting on repositories.', + '', + `- Failed run: ${runUrl}`, + '- Runbook: admin/OWNED_PROVISIONING.md', + '', + 'This issue closes automatically on the next successful run.' + ].join('\n') + }); + } + + - name: Close provisioning alert issue on success + if: ${{ success() }} + uses: actions/github-script@v8 + with: + script: | + const open = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: 'provisioning-alert', + state: 'open', + per_page: 10 + }); + for (const issue of open.data) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + body: 'Provisioning succeeded; closing this alert.' + }); + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + state: 'closed', + state_reason: 'completed' + }); + } diff --git a/.github/workflows/provisioning-health-check.yml b/.github/workflows/provisioning-health-check.yml new file mode 100644 index 00000000..db2e0cb8 --- /dev/null +++ b/.github/workflows/provisioning-health-check.yml @@ -0,0 +1,91 @@ +name: Provisioning Credentials Health Check + +# Weekly canary for the provisioning GitHub App: mints an installation token +# and verifies the learning-room template repository is reachable. Makes no +# changes. Catches expired keys, uninstalled Apps, and permission drift before +# a learner does, instead of the failure surfacing mid-enrollment. + +on: + workflow_dispatch: + schedule: + - cron: '23 6 * * 1' + +permissions: + contents: read + issues: write + +jobs: + check-auth: + name: Mint token and verify template access + runs-on: ubuntu-latest + environment: provisioning + steps: + - name: Check out workshop repo (provisioning scripts) + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Check provisioning credentials + run: node .github/scripts/provisioning/provision-cli.js --check-auth + env: + PROVISIONING_MODE: ${{ vars.PROVISIONING_MODE }} + LEARNING_ROOM_TEMPLATE_REPO: ${{ vars.LEARNING_ROOM_TEMPLATE_REPO }} + PROVISIONING_STUDENT_OWNER: ${{ vars.PROVISIONING_STUDENT_OWNER }} + GITHUB_API_URL: ${{ github.api_url }} + PROVISIONING_APP_ID: ${{ secrets.PROVISIONING_APP_ID }} + PROVISIONING_APP_PRIVATE_KEY: ${{ secrets.PROVISIONING_APP_PRIVATE_KEY }} + PROVISIONING_APP_INSTALLATION_ID: ${{ secrets.PROVISIONING_APP_INSTALLATION_ID }} + + - name: Open or update provisioning alert issue + if: ${{ failure() }} + uses: actions/github-script@v8 + with: + script: | + const label = 'provisioning-alert'; + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + color: 'b60205', + description: 'Automated alert: learning room provisioning is failing' + }); + } catch (e) { /* label already exists */ } + + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + const open = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + labels: label, + state: 'open', + per_page: 1 + }); + + if (open.data.length) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: open.data[0].number, + body: `Credentials health check failed: ${runUrl}` + }); + } else { + await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: 'Provisioning credentials health check is failing', + labels: [label], + body: [ + '## Provisioning watchdog', + '', + 'The weekly credentials health check failed. Provisioning will fail for the next enrollee.', + '', + `- Failed run: ${runUrl}`, + '- Runbook: admin/OWNED_PROVISIONING.md', + '', + 'This issue closes automatically on the next successful provisioning run.' + ].join('\n') + }); + } diff --git a/.github/workflows/registration.yml b/.github/workflows/registration.yml index 04eef62d..8c52bf11 100644 --- a/.github/workflows/registration.yml +++ b/.github/workflows/registration.yml @@ -8,6 +8,8 @@ on: permissions: contents: write issues: write + # Needed to dispatch the provisioning workflow when an enrollment lands. + actions: write jobs: classroom-enrollment: @@ -50,7 +52,6 @@ jobs: uses: actions/github-script@v8 env: ORIGINAL_ISSUE: ${{ steps.dup-check.outputs.original_issue }} - DAY1_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY1_ASSIGNMENT_URL }} with: script: | function parseField(body, fieldName) { @@ -65,19 +66,14 @@ jobs: const body = context.payload.issue.body || ''; const displayName = parseField(body, 'Full Name') || 'there'; - const links = []; - const day1 = (process.env.DAY1_ASSIGNMENT_URL || '').trim(); - if (day1) links.push(`- Day 1 assignment link: ${day1}`); - - const quickLinksSection = links.length - ? `\n\n## Quick links\n${links.join('\n')}` - : ''; const duplicateBodyLines = [ '## You are already enrolled', '', `Hi ${displayName}, it looks like you already submitted enrollment in #${process.env.ORIGINAL_ISSUE}.`, '', - `No action needed. Your original enrollment remains active.${quickLinksSection}`, + 'No action needed. Your original enrollment remains active. If you have not received', + 'your learning room invitation yet, check your GitHub notifications, or ask for help:', + '- https://github.com/Community-Access/support/issues', '', '## Where to see replies', '- GitHub notification bell', @@ -354,8 +350,6 @@ jobs: steps.storage-guardrail.outputs.configured == 'true' && steps.parse.outputs.is_valid == 'true' uses: actions/github-script@v8 - env: - DAY1_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY1_ASSIGNMENT_URL }} with: script: | function parseField(body, fieldName) { @@ -370,21 +364,14 @@ jobs: const body = context.payload.issue.body || ''; const displayName = parseField(body, 'Full Name') || 'there'; - const day1 = (process.env.DAY1_ASSIGNMENT_URL || '').trim(); - - const onboarding = []; - if (day1) onboarding.push(`- Day 1 assignment: ${day1}`); - - const noLinksFallback = onboarding.length - ? '' - : '\n- Your enrollment was received. A facilitator will share your classroom link shortly.'; const successBodyLines = [ '## You are enrolled. Welcome!', - '', `Hi ${displayName}, great job submitting your first step.`, '', '## Next steps', - `${onboarding.join('\n')}${noLinksFallback}`, + '- Your private learning room repository is being prepared automatically.', + '- Watch for a GitHub repository invitation (notification bell and email). Accept it to open your learning room.', + '- While you wait, start with the Pre-Workshop Setup Guide: https://community-access.github.io/git-going-with-github/docs/00-pre-workshop-setup.html', '', 'Day 2 content automatically unlocks in your learning room after you complete Day 1 challenges.', '', @@ -392,10 +379,10 @@ jobs: '- GitHub notification bell', '- Email notifications for this issue thread', '', - 'Please reply `ack` on this issue after you confirm your Day 1 link works.', + 'Please reply `ack` on this issue after you receive your learning room invitation.', 'Then close this enrollment issue as Challenge 0.', '', - 'If you get stuck at any step, open support:', + 'If you do not receive an invitation within an hour, or you get stuck at any step, open support:', '- https://github.com/Community-Access/support/issues', '', 'You belong here.' @@ -408,6 +395,29 @@ jobs: body: successBodyLines.join('\n') }); + - name: Trigger learning room provisioning + if: >- + steps.dup-check.outputs.is_duplicate == 'false' && + steps.storage-guardrail.outputs.configured == 'true' && + steps.parse.outputs.is_valid == 'true' + uses: actions/github-script@v8 + with: + script: | + // Event-driven provisioning: the provisioning workflow also runs on + // a healing-sweep schedule, so a failed dispatch only delays a + // learner, never strands them. Do not fail enrollment over it. + try { + await github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'provision-learning-rooms.yml', + ref: 'main' + }); + console.log('Provisioning workflow dispatched.'); + } catch (e) { + console.log(`Could not dispatch provisioning workflow: ${e.message}`); + } + - name: Add enrolled label if: >- steps.dup-check.outputs.is_duplicate == 'false' && @@ -614,9 +624,6 @@ jobs: - name: Post welcome comment if: steps.dup-check.outputs.is_duplicate == 'false' && steps.capacity-check.outputs.is_full == 'false' uses: actions/github-script@v8 - env: - DAY1_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY1_ASSIGNMENT_URL }} - DAY2_ASSIGNMENT_URL: ${{ vars.CLASSROOM_DAY2_ASSIGNMENT_URL }} with: script: | function parseField(body, fieldName) { @@ -634,14 +641,6 @@ jobs: const lastName = parseField(body, 'Last Name'); const fullName = [firstName, lastName].filter(Boolean).join(' ').trim() || parseField(body, 'Full Name') || parseField(body, 'Name'); const displayName = fullName || 'there'; - const day1 = (process.env.DAY1_ASSIGNMENT_URL || '').trim(); - const day2 = (process.env.DAY2_ASSIGNMENT_URL || '').trim(); - - let classroomSection = ''; - if (day1) { - const lines = ['## Classroom Access', `- Start here: ${day1}`]; - classroomSection = `\n\n${lines.join('\n')}`; - } await github.rest.issues.createComment({ owner: context.repo.owner, @@ -651,11 +650,7 @@ jobs: Hi ${displayName}, 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). - - ${classroomSection} + Your registration is confirmed. 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). ## Where replies appear - GitHub notification bell @@ -665,7 +660,7 @@ jobs: - Issues: https://github.com/Community-Access/support/issues - Discussions: https://github.com/Community-Access/support/discussions - See you at the workshop!` + You belong here.` }); - name: Add registration label @@ -684,6 +679,89 @@ jobs: console.log('Label may already exist:', e.message); } + # PII hygiene: this is a public repository. Mirror the [ENROLL] pattern - + # store the full intake privately, then redact the public issue body so + # names and email addresses do not live in public issues indefinitely. + - name: Store registration intake in private repo + id: store-intake + if: >- + steps.dup-check.outputs.is_duplicate == 'false' && + steps.capacity-check.outputs.is_full == 'false' && + vars.PRIVATE_STUDENT_DATA_REPO != '' + uses: actions/github-script@v8 + env: + TARGET_REPO: ${{ vars.PRIVATE_STUDENT_DATA_REPO }} + with: + github-token: ${{ secrets.PRIVATE_STUDENT_DATA_TOKEN }} + script: | + 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; + } + + const target = (process.env.TARGET_REPO || '').trim(); + if (!target.includes('/')) { + core.setOutput('stored', 'false'); + return; + } + + const body = context.payload.issue.body || ''; + const firstName = parseField(body, 'First Name'); + const lastName = parseField(body, 'Last Name'); + const [owner, repo] = target.split('/'); + const intakeLines = [ + '## Workshop Registration Intake', + '', + `- Public issue: https://github.com/${context.repo.owner}/${context.repo.repo}/issues/${context.issue.number}`, + `- Submitted by: @${context.payload.issue.user.login}`, + `- First Name: ${firstName}`, + `- Last Name: ${lastName}`, + `- Email Address: ${parseField(body, 'Email Address')}`, + `- GitHub Proficiency Level: ${parseField(body, 'GitHub Proficiency Level')}`, + `- Primary Screen Reader: ${parseField(body, 'Primary Screen Reader')}`, + `- Questions or Accommodations: ${parseField(body, 'Questions or Accommodations')}`, + '', + `- Captured at: ${new Date().toISOString()}` + ]; + + try { + await github.rest.issues.create({ + owner, + repo, + title: `[REGISTER-INTAKE] ${[firstName, lastName].filter(Boolean).join(' ') || context.payload.issue.user.login}`, + body: intakeLines.join('\n') + }); + core.setOutput('stored', 'true'); + } catch (error) { + console.log(`Could not store private intake: ${error.message}`); + core.setOutput('stored', 'false'); + } + + # Only redact once the data is safely captured privately; otherwise leave + # the issue as-is so no information is lost. + - name: Redact public registration issue body + if: steps.store-intake.outputs.stored == 'true' + uses: actions/github-script@v8 + with: + script: | + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: [ + '## Registration submission (redacted)', + '', + '- Status: accepted', + `- Submitted by: @${context.payload.issue.user.login}`, + `- Processed at: ${new Date().toISOString()}` + ].join('\n') + }); + export-csv: name: Export Registrations to CSV runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index c8f566d8..91c55fde 100644 --- a/.gitignore +++ b/.gitignore @@ -192,3 +192,7 @@ learning-room-e2e-*.json # VS Code Local History extension .history/ + +# Scratch artifacts - never commit +tmp-* +learning-room-e2e-*.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..86ef46ae --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,50 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Repository Is + +"GIT Going with GitHub" - an accessible, screen-reader-first workshop teaching blind and low-vision users to contribute to open source on GitHub. This is primarily a documentation project (Markdown in `docs/`) with three supporting systems: an HTML build pipeline, a podcast audio pipeline, and GitHub Actions automation that provisions and grades per-student "Learning Room" repositories. + +## Commands + +```bash +npm run build:html # Regenerate html/ from Markdown (also regenerates work.md) +npm run watch:html # Auto-rebuild HTML on Markdown changes +npm run clean # Delete html/ + +npm run test:automation # node --test .github/scripts/__tests__/*.test.js +npm run test:provisioning # node --test .github/scripts/provisioning/__tests__/*.test.js +npm run test:llm-execution # Podcast LLM job execution tests + +# Single test file: +node --test .github/scripts/provisioning/__tests__/roster.test.js + +npm run validate:podcasts # Podcast catalog + listening-order validation +npm run validate:authoritative-sources # Checks "Authoritative Sources" sections in docs +``` + +Tests use the built-in Node test runner (Node >= 20), no test framework dependency. The podcast audio/TTS steps (`build:podcast-audio`, metadata, inventory) require Python and ffmpeg and run locally; treat them as out of scope unless asked. + +## Critical Workflow: HTML Is Committed, Not CI-Built + +There is no CI build for the docs. After editing any `.md` file, run `npm run build:html` and commit both the Markdown source and the regenerated `html/` output in the same commit. `docs/*.md` -> `html/docs/*.html`, `README.md` -> `html/index.html`, plus `learning-room/` and root-level docs. The build template and CSS live in `scripts/build-html.js`. + +## Architecture + +- `docs/` - The curriculum: chapters `00`-`22` plus `appendix-*.md`, `CHALLENGES.md` (challenge hub), and `solutions/` (reference solutions). Note: appendix display letters (A-Z in README tables) do NOT match appendix filenames - they were renumbered; go by the README tables, not the filename letter. +- `learning-room/` - Template repository copied into each student's private repo during provisioning. Its own `.github/` holds the student-facing automation (challenge issue templates, validation bot, progression workflows). `scripts/generate-work-md.js` derives `work.md` from these challenge templates. +- `.github/scripts/provisioning/` - "Hybrid provisioning": the GitHub-native replacement for GitHub Classroom (which was removed entirely). `provision-core.js` is pure orchestration - it takes an injected client (`github-client.js`), never touches the network or secrets directly, and returns an updated roster plus a log. Tests run it against a fake client. `github-app-auth.js` handles GitHub App authentication; `roster.js` and `roster.schema.json` define the student roster; `provision-cli.js` is the entry point; the workflow is `provision-learning-rooms.yml`. The algorithm (serial, backoff, idempotent) is specified in SPEC.md section 7.2b - keep code and spec in sync. +- `.github/scripts/` (top level) - PR validation and challenge-progression bots. Some are legacy from the old shared-repository model; see `.github/README.md` for which workflows are active vs. legacy. +- `podcasts/` - Audio companion pipeline: `build-bundles.js` -> per-episode source bundles -> scripts -> local ONNX TTS (Kokoro default, Piper fallback) -> `generate-site.js` builds the player page and RSS feed. `config/listening-order.json` is the canonical episode order shared by JS (`lib/listening-plan.js`) and Python (`listening_plan.py`) resolvers. Audio files are gitignored and hosted on GitHub Releases. +- `scripts/` - Repo-level build tooling (HTML, ePub, BRF, diagram SVGs, source validation). + +Root-level `tmp-*` files, `work.md`/`work.html`, and `golden.md`/`golden.html` are generated or scratch artifacts - do not hand-edit them. + +## Conventions + +- Never use emojis anywhere: docs, code, comments, commit messages (from `.github/copilot-instructions.md`). +- All documentation is written screen-reader-first: semantic headings, descriptive link text, keyboard-only instructions, no meaning conveyed by visuals alone. +- Docs end with an "Authoritative Sources" section plus a "Section-Level Source Map"; `npm run validate:authoritative-sources` checks these. Preserve them when editing docs. +- Commit messages in this repo follow conventional-commit style (`docs:`, `build:`, `refactor:`, ...). +- License is CC BY 4.0. diff --git a/DEPLOYMENT_ASSESSMENT.md b/DEPLOYMENT_ASSESSMENT.md index 57407024..d82e69bd 100644 --- a/DEPLOYMENT_ASSESSMENT.md +++ b/DEPLOYMENT_ASSESSMENT.md @@ -11,19 +11,19 @@ The Hybrid provisioning system is **architecturally sound, hardened, and product ## Hardening Assessment by Component -### ✅ **GitHub App Configuration** +### OK: **GitHub App Configuration** - **Status:** HARDENED - **Why:** Fine-grained permissions (least privilege), secrets stored in GitHub Actions only, PEM key never in code - **Risk:** None identified - **Action:** READY -### ✅ **Secrets & Variables Management** +### OK: **Secrets & Variables Management** - **Status:** HARDENED - **Why:** All three secrets (App ID, Installation ID, PEM) stored securely in GitHub repository Actions secrets; variables configured correctly - **Risk:** None identified - **Action:** READY -### ✅ **Infrastructure Code (Provisioning Scripts)** +### OK: **Infrastructure Code (Provisioning Scripts)** - **Status:** HARDENED - **Why:** - Idempotent: Re-running is safe (already-exists vs. created state) @@ -33,14 +33,14 @@ The Hybrid provisioning system is **architecturally sound, hardened, and product - **Risk:** None identified - **Action:** READY -### ✅ **Documentation Updates** +### OK: **Documentation Updates** - **Status:** COMPLETE - **What:** 15+ files updated, 37+ files deleted, HTML regenerated on both sites - **Coverage:** Student-facing, facilitator, operator all covered - **Risk:** None identified - **Action:** READY -### ✅ **Public Site Deployments** +### OK: **Public Site Deployments** - **Status:** COMPLETE - **Coverage:** - community-access.org/git-going-with-github (auto via GitHub Pages) @@ -49,7 +49,7 @@ The Hybrid provisioning system is **architecturally sound, hardened, and product - **Risk:** None identified - **Action:** READY -### ⚠️ **End-to-End Student Workflow (NOT YET TESTED)** +### Warning: **End-to-End Student Workflow (NOT YET TESTED)** - **Status:** UNTESTED - **What's missing:** 1. Admin roster repo creation @@ -57,25 +57,25 @@ The Hybrid provisioning system is **architecturally sound, hardened, and product 3. Verification that provision workflow runs end-to-end 4. Verification that invitation email sent to test account 5. Verification that student can accept invite and access repo -- **Risk:** Medium—logical gaps possible, though architecture is solid -- **Action:** REQUIRED—Run Phase 4 before admitting real students +- **Risk:** Medium-logical gaps possible, though architecture is solid +- **Action:** REQUIRED-Run Phase 4 before admitting real students --- -## What Has Been Tested ✅ +## What Has Been Tested -- ✅ Automation tests: 78/78 passing (was 98, reduced after removing Classroom tests) -- ✅ Provisioning tests: 55/55 passing -- ✅ HTML regeneration: 393 markdown files → HTML -- ✅ Site deployments: Both production sites live -- ✅ GitHub App creation: Properly configured with correct permissions -- ✅ Secrets storage: All three values stored securely +- OK: Automation tests: 78/78 passing (was 98, reduced after removing Classroom tests) +- OK: Provisioning tests: 55/55 passing +- OK: HTML regeneration: 393 markdown files -> HTML +- OK: Site deployments: Both production sites live +- OK: GitHub App creation: Properly configured with correct permissions +- OK: Secrets storage: All three values stored securely --- -## What Has NOT Been Tested ❌ +## What Has NOT Been Tested -- ❌ **End-to-end provisioning flow** with a real test student +- Missing: **End-to-end provisioning flow** with a real test student - Enrollment form submission - Issue creation in test account - Automation comment with learning room link @@ -154,14 +154,14 @@ The Hybrid provisioning system is **architecturally sound, hardened, and product ## Failsafety Verdict -### ✅ System is FAILSAFE for: +### System is FAILSAFE for: - **Re-running provisioning**: Safe, idempotent, no duplicates - **Recovering from failures**: Re-run workflow, roster is source of truth - **Credential compromise**: GitHub App scope limited, PEM in Actions only - **Data loss**: Audit trail in provisioning-log.json, roster can be rebuilt from git history - **Classroom service issues**: No longer dependent on GitHub Classroom -### ⚠️ System NEEDS TESTING for: +### System NEEDS TESTING for: - **Real student enrollment**: End-to-end flow not yet validated - **Scale**: Tested with unit tests (78 automation, 55 provisioning), not with 50+ real students - **Edge cases**: Duplicate submissions, roster conflicts, network timeouts during provisioning @@ -189,3 +189,33 @@ The Hybrid provisioning system is **architecturally sound, hardened, and product *Assessment completed June 2, 2026* *System: Hybrid Provisioning v1.0* + +--- + +## Correction (July 2, 2026) + +This assessment's verdict did not hold. The Phase 4 smoke test listed above as MUST DO was never completed: the provisioning GitHub App was created but never installed on the Community-Access organization, so every non-dry-run provisioning attempt from June 2 onward failed while minting an installation token (HTTP 404), and two enrolled learners remained unprovisioned for a month with no alert. + +Lessons now encoded in the workflow and runbook: + +- An untested MUST DO item means the status is NOT READY. The smoke test is the release gate; no schedule is enabled until a real (non-dry-run) run has succeeded once. +- Every READY claim in an assessment must cite evidence (a green run ID), especially for credentials. +- Scheduled workflows must alert on failure. The provisioning workflow now opens a watchdog issue on any failure, and a weekly credentials health check catches drift between enrollments. + +See REVIEW-2026-07-02.md for the full incident review. + +## Authoritative Sources + +Use these official references when you need the current source of truth for facts in this chapter. + +- [GitHub Docs, home](https://docs.github.com/en) +- [GitHub Changelog](https://github.blog/changelog/) + +### Section-Level Source Map + +Use this map to verify facts for each major section in this file. + +- **Hardening Assessment by Component:** [GitHub Apps documentation](https://docs.github.com/en/apps) +- **Failsafe Mechanisms:** [GitHub Actions documentation](https://docs.github.com/en/actions) +- **Recommendations Before Go-Live:** [Installing GitHub Apps](https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party) +- **Correction (July 2, 2026):** [GitHub REST API, create an installation access token](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app) diff --git a/REVIEW-2026-07-02.md b/REVIEW-2026-07-02.md new file mode 100644 index 00000000..bfe1d9e3 --- /dev/null +++ b/REVIEW-2026-07-02.md @@ -0,0 +1,130 @@ +# Operations Review: Provisioning Failures and Workshop Model Robustness + +Date: July 2, 2026 +Scope: GitHub Actions failure analysis, provisioning subsystem, registration pipeline, repo hygiene. + +Implementation status (July 2, 2026): everything in sections 3, 4.1-4.4, and 6 that is +code has been implemented (installation-ID auto-discovery, watchdog alert issue, +work-free early exit, event-driven provisioning dispatch, weekly credentials health +check, preflight assertions, intake-to-roster sync, Classroom link removal, [REGISTER] +private intake plus redaction, authoritative-sources gate fixes, scratch cleanup). +The remaining manual step is section 2: install the GitHub App on the +Community-Access organization. Template drift (4.5) is still open. + +## 1. Headline Finding: The Provisioning GitHub App Is Not Installed on the Org + +The "Provision Learning Rooms" workflow has failed on every real run since it was deployed. Verified numbers from the GitHub API: + +- 280 total runs of `provision-learning-rooms.yml`. +- Exactly 1 success ever (June 2, 2026, 20:16 UTC) - and that run was a dry run, which skips authentication entirely. +- Every non-dry run, including roughly 12 scheduled runs per day for the past 30 days, fails with: + `Failed to mint installation token (HTTP 404)` + +Root cause, confirmed directly: `GET /orgs/Community-Access/installations` shows exactly one GitHub App installed on the org - `digitalocean` (app id 64711). The provisioning App was created (App ID and PEM are stored as secrets) but was never installed on the Community-Access organization. There is no installation to mint a token against, so the token mint returns 404 no matter what `PROVISIONING_APP_INSTALLATION_ID` contains. + +Consequences, also confirmed: + +- The private roster (`git-going-with-github-administration/roster.json`) has 2 learners in `provision_state: pending` - unchanged since June 2. Two registered people have been waiting a month for learning rooms that were never created. +- The admin repo has had no roster commits since June 2 because the workflow dies before the commit step. +- This is precisely the failure class SPEC.md's golden principle warns about ("never let a single point of failure hold a cohort hostage") and violates the section 7.2b invariant "any failure visible to a human before a learner notices." The watchdog the spec calls for does not exist. + +Note on the local working-tree changes: the uncommitted edits to `github-app-auth.js` (normalize URL-form installation IDs, better 404 hint) are good hardening and all 58 provisioning tests pass, but they will not fix production. No installation ID is valid until the App is installed. + +## 2. Immediate Fix (Operator Steps, ~15 Minutes) + +1. Install the App: GitHub -> Organization settings -> Developer settings -> GitHub Apps -> your provisioning App -> Install App -> Community-Access. Because the App must create new repositories, install it org-wide (repository creation cannot be granted through a selected-repositories installation). +2. Capture the installation ID from the post-install URL (`.../settings/installations/`) or from `GET /app/installations` using an App JWT. +3. Update the `PROVISIONING_APP_INSTALLATION_ID` secret (it lives in the `provisioning` environment, not repo-level secrets). +4. Run the workflow manually with `dry_run: true` (expect: "2 learner(s) would be provisioned"), then run it for real. +5. Verify: two new `learning-room-*` repos, invitations sent, roster entries flipped to `provisioned`, `provisioning-log.json` committed to the admin repo. + +## 3. Make the Failure Class Impossible: Code Changes + +These are ordered by payoff. + +### 3.1 Auto-discover the installation ID + +Stop storing the installation ID as a secret at all. With an App JWT you can call `GET /app/installations` and select the installation whose account matches `PROVISIONING_STUDENT_OWNER`. Add this as the fallback (or the default) in `github-app-auth.js` / `provision-cli.js`. This removes an entire class of copy-paste misconfiguration and survives App re-installation (installation IDs change when an App is uninstalled and reinstalled). + +Alternative for the workflow path: the official `actions/create-github-app-token` action needs only `app-id` and `private-key` and resolves the installation from the repo owner automatically. It keeps the zero-npm-dependency property of the repo since it runs as an action, and the CLI keeps the hand-rolled path for local use. + +### 3.2 Alert on failure - implement the spec's watchdog + +Thirty days of silent failure proves the need. Add a final `if: failure()` step to `provision-learning-rooms.yml` that creates or updates a pinned, labeled issue (for example `provisioning-broken`) in this repo, including the error line from the log. Close it automatically on the next success. Requires `issues: write` on this workflow only. + +### 3.3 Skip work-free runs + +Most of the 280 failed runs had zero learners to provision. Read the roster first; if there are no `pending`/`failed` entries, exit success before minting a token. Fewer runs, less noise, and credential problems surface only when they matter - paired with 3.5 so they still surface promptly. + +### 3.4 Event-driven provisioning instead of 30-minute polling + +The registration workflow already fires on `issues: opened`. Have the enrollment path send a `repository_dispatch` (or `workflow_dispatch`) to trigger provisioning immediately, and relax the cron to a few times per day as a healing sweep. Students get rooms in minutes instead of up to 30 (in practice, GitHub throttles the 30-minute cron to roughly every 2 hours anyway), and scheduled load drops by 90 percent. + +### 3.5 Credential health check + +A small weekly job that mints a token and makes one read call (`GET /app`), with the same failure-alert step. Catches expired keys, uninstalled Apps, and permission drift before a learner does. + +### 3.6 Preflight assertions + +Before iterating the roster, `provision-cli.js` should verify the minted token can read `LEARNING_ROOM_TEMPLATE_REPO`. Fail with one clear message instead of per-learner errors. + +## 4. Pipeline Gaps in the Workshop Model + +### 4.1 Registration does not reach the provisioning roster + +The chain today: `[ENROLL]` issue -> intake issue in the private admin repo -> (manual, undocumented step) -> `roster.json` -> provisioning. Nothing automates the middle hop. The 2 pending learners suggest roster entries were hand-written once on June 2 and never again. Recommendation: a workflow in the admin repo that converts intake issues into `pending` roster entries (idempotent on github_handle+cohort), or teach `provision-cli.js` to read intake issues directly. Until then, document the manual step in the runbook so it is at least a known duty. + +### 4.2 New registrants are being sent dead Classroom links + +`CLASSROOM_DAY1_ASSIGNMENT_URL` / `CLASSROOM_DAY2_ASSIGNMENT_URL` repo variables still point at `classroom.github.com/a/...` and the registration welcome comment still sends people there - but Classroom was removed from the program in the June 2 transition. Anyone registering today gets a broken onboarding path. Replace these variables and the welcome text with the Hybrid flow (enroll issue -> learning room invitation), or clear the vars so the fallback text ("a facilitator will share your link") is used. + +### 4.3 PII in public issue bodies + +The `[ENROLL]` path redacts the public issue body after capturing intake privately - good. The `[REGISTER]` path does not: full name and email remain in public issue bodies indefinitely. Apply the same redact-after-capture pattern to `[REGISTER]`, and consider a backfill pass over historical registration issues. + +### 4.4 Two overlapping intake paths + +`[REGISTER]` (waitlist/welcome/CSV) and `[ENROLL]` (intake/provisioning) coexist with different field schemas and different privacy behavior. For the self-paced era, collapse to one form that captures minimal public data (GitHub handle only), stores the rest privately, and feeds the roster automatically. + +### 4.5 Template drift risk + +The workshop repo carries `learning-room/` while provisioning seeds from the separate `Community-Access/learning-room-template` repo (last pushed May 22). Two sources of truth will drift. Either add a sync workflow (this repo -> template repo on change to `learning-room/**`) or delete one copy and point everything at the survivor. + +## 5. Process Finding: The Assessment Predicted This + +`DEPLOYMENT_ASSESSMENT.md` (June 2) declared the system "READY FOR PRODUCTION" while flagging, in its own MUST-DO section, that the end-to-end smoke test had not been run. The smoke test would have failed in under a minute and pointed at the missing installation. The lesson to encode in the runbook and QA gate: an untested MUST-DO item means the status is NOT READY - the smoke test is the gate, and no schedule gets enabled until a real (non-dry-run) invocation has succeeded once. The assessment also states "Secrets & Variables: HARDENED, Risk: None identified" for a credential set that had never authenticated successfully; future assessments should require evidence (a green run ID) next to every READY claim. + +## 6. Repo Hygiene and Smaller Findings + +- `validate-authoritative-sources` fails at HEAD: `DEPLOYMENT_ASSESSMENT.md` and `fix.md` are missing the required "Authoritative Sources" and "Section-Level Source Map" sections. Every docs push will keep failing this gate until fixed (add sections, or exclude assessment/scratch files in `scripts/validate-authoritative-sources.js`). +- `DEPLOYMENT_ASSESSMENT.md` is full of emoji, violating the repo's own no-emoji rule in `.github/copilot-instructions.md`. +- Root clutter: `tmp-*.csv`, `tmp-*.log`, `tmp-*.ps1`, `fix.md`/`fix.html`, `key.txt` (verify this is not a credential; if it ever held one, rotate it), and two `learning-room-e2e-*.json` files sit at repo root. Move to an untracked scratch area or delete; add `tmp-*` to `.gitignore`. +- `PROVISIONING_TOKEN` (actions-bot spike mode) is still wired into the production workflow env, empty. The spec calls the PAT path "throwaway." Remove it from the workflow to shrink the config surface. +- Actions runners warn that `actions/checkout@v4` and `actions/setup-node@v4` target deprecated Node 20; bump to the current majors during the next maintenance pass. +- `learning-room-pr-bot` shows occasional `action_required` conclusions (first-time fork contributors awaiting approval) - expected, but worth a facilitator habit of clearing the queue. + +## 7. Suggested Order of Work + +1. Install the App, fix the secret, provision the 2 waiting learners (section 2). Everything else is secondary to unblocking real people. +2. Commit the local `github-app-auth.js` hardening plus installation-ID auto-discovery (3.1). +3. Add failure alerting to the provisioning workflow (3.2) - small diff, permanent visibility. +4. Fix the dead Classroom links in registration (4.2) and the `[REGISTER]` PII redaction (4.3). +5. Fix the `validate-authoritative-sources` gate (section 6) so CI is green at HEAD. +6. Event-driven provisioning + registration-to-roster automation (3.4, 4.1) - this is the "robust model": one intake form, automatic roster append, provision-on-registration, healing sweep cron, watchdog issue on any failure, weekly credential health check. + +## Authoritative Sources + +Use these official references when you need the current source of truth for facts in this chapter. + +- [GitHub Docs, home](https://docs.github.com/en) +- [GitHub Changelog](https://github.blog/changelog/) + +### Section-Level Source Map + +Use this map to verify facts for each major section in this file. + +- **Headline Finding:** [GitHub REST API, list app installations for an organization](https://docs.github.com/en/rest/orgs/orgs#list-app-installations-for-an-organization), [GitHub REST API, create an installation access token](https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app) +- **Immediate Fix:** [Installing GitHub Apps](https://docs.github.com/en/apps/using-github-apps/installing-a-github-app-from-a-third-party) +- **Code Changes:** [actions/create-github-app-token](https://github.com/actions/create-github-app-token), [GitHub REST API, list installations for the authenticated app](https://docs.github.com/en/rest/apps/apps#list-installations-for-the-authenticated-app) +- **Pipeline Gaps:** [GitHub Actions, repository_dispatch event](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#repository_dispatch) +- **Repo Hygiene:** [GitHub Actions runner Node 20 deprecation](https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/) diff --git a/SPEC.md b/SPEC.md index 68ed9786..84d8934e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -223,9 +223,10 @@ Grant only these. Anything beyond this list is over-privileged and fails the sec App configuration rules: -- Install the App only on the `Community-Access` organization, scoped to the template and student repositories. -- Store `PROVISIONING_APP_ID` and `PROVISIONING_APP_PRIVATE_KEY` in GitHub Secrets; never in code or public repos. +- Install the App on the `Community-Access` organization with all-repositories access (creating student repositories requires an organization-wide installation). An App that exists but is not installed fails every token mint with HTTP 404. +- Store `PROVISIONING_APP_ID` and `PROVISIONING_APP_PRIVATE_KEY` in GitHub Secrets; never in code or public repos. `PROVISIONING_APP_INSTALLATION_ID` is optional: when unset, provisioning discovers the installation from the App at runtime, which removes a copy-paste failure mode and survives re-installation. - Mint a short-lived installation token at the start of each provisioning run; never persist it. +- Verify credentials independently of enrollment: a weekly health check mints a token and confirms template access, and any provisioning failure opens a `provisioning-alert` issue (the watchdog required by section 7.3). - Document a private-key rotation procedure and rotate on any suspected exposure. ### 7.2b Provisioning algorithm (idempotent, serial) diff --git a/admin/HYBRID_DEPLOYMENT_GUIDE.md b/admin/HYBRID_DEPLOYMENT_GUIDE.md index 18d9665a..74dab916 100644 --- a/admin/HYBRID_DEPLOYMENT_GUIDE.md +++ b/admin/HYBRID_DEPLOYMENT_GUIDE.md @@ -96,12 +96,20 @@ mints short-lived tokens, and uses fine-grained least-privilege permissions - Issues: Read and write (optional; only if the App seeds the first issue). 5. Create the App, then note the numeric App ID. 6. Generate a private key; a `.pem` file downloads. Keep it secret. -7. Install the App on the `Community-Access` organization, scoped to the template - repository and the student repositories (or all repositories if simpler for now). +7. Install the App on the `Community-Access` organization with **All repositories** + access. Creating new student repositories requires an organization-wide + installation; a selected-repositories installation cannot create repos. This step + is mandatory: an App that exists but is not installed fails every token mint with + HTTP 404, and nothing downstream can work. 8. Open the installation and note the numeric Installation ID (it is in the - installation settings URL). + installation settings URL). Storing it is optional - provisioning discovers it + automatically when the secret is unset. -Verification: you now hold three values: App ID, Installation ID, and the PEM key. +Verification: you now hold three values: App ID, Installation ID (optional), and the +PEM key. Confirm they work before continuing: run the **Provisioning Credentials +Health Check** workflow, or locally +`node .github/scripts/provisioning/provision-cli.js --check-auth`. Do not proceed to +Phase 4 until the check passes. ## Phase 3: Configure variables and secrets @@ -116,13 +124,14 @@ Repository or environment variables: | `LEARNING_ROOM_TEMPLATE_REPO` | `Community-Access/learning-room-template` | | `PROVISIONING_STUDENT_OWNER` | `Community-Access` | | `ADMIN_ROSTER_REPO` | `Community-Access/glow-admin` (your admin repo) | +| `PROVISIONING_COHORT_ID` | Cohort that new enrollees are added to | Secrets: | Secret | Value | |---|---| | `PROVISIONING_APP_ID` | The App ID from Phase 2 | -| `PROVISIONING_APP_INSTALLATION_ID` | The Installation ID from Phase 2 | +| `PROVISIONING_APP_INSTALLATION_ID` | Optional. The Installation ID from Phase 2; when unset, provisioning discovers it from the App installations | | `PROVISIONING_APP_PRIVATE_KEY` | The full contents of the `.pem` file | | `PRIVATE_STUDENT_DATA_TOKEN` | A token that can check out and push to the admin repo | @@ -142,7 +151,7 @@ rehearse key rotation; rotate on any suspected exposure. The provisioning workflow ships in this repository at [../.github/workflows/provision-learning-rooms.yml](../.github/workflows/provision-learning-rooms.yml). -Merging this branch deploys it. It runs on a 30-minute schedule and on demand. +Merging this branch deploys it. It runs when registration dispatches it, on a healing-sweep schedule, and on demand. Smoke test with a single test account before any real cohort: @@ -238,7 +247,7 @@ Do not open a cohort until every item holds. This complements the release gate i ## Operating a cohort -- **Provision on registration, not big-bang.** Let the 30-minute schedule trickle new +- **Provision on registration, not big-bang.** Let the registration dispatch and healing sweep trickle new learners in. This keeps you far below any rate limit regardless of cohort size. - **Watch the roster and log.** `failed` entries and `error` log lines are your signal to intervene; re-running provisioning heals pending and failed entries. diff --git a/admin/OWNED_PROVISIONING.md b/admin/OWNED_PROVISIONING.md index 73f304b9..5bb3b626 100644 --- a/admin/OWNED_PROVISIONING.md +++ b/admin/OWNED_PROVISIONING.md @@ -89,18 +89,28 @@ mints short-lived tokens, and uses fine-grained least-privilege permissions. 1. Create a GitHub App in the `Community-Access` organization with only these permissions: Repository administration (write), Contents (write), Metadata (read), and optionally Issues (write). Nothing more. -2. Install the App on the organization, scoped to the template and student repositories. +2. Install the App on the organization. Creating student repositories requires an + organization-wide installation (repository creation cannot be granted through a + selected-repositories installation). Do not skip this step: an App that exists but + is not installed fails every token mint with HTTP 404. 3. Generate a private key (PEM). Store these as repository or environment secrets: - `PROVISIONING_APP_ID` - `PROVISIONING_APP_PRIVATE_KEY` (the PEM contents) - - `PROVISIONING_APP_INSTALLATION_ID` + - `PROVISIONING_APP_INSTALLATION_ID` (optional; when unset, provisioning discovers + the installation from the App itself, which also survives re-installation) 4. Set repository variables: - `PROVISIONING_MODE` = `github-app` - `LEARNING_ROOM_TEMPLATE_REPO` = `Community-Access/learning-room-template` - `PROVISIONING_STUDENT_OWNER` = `Community-Access` - `ADMIN_ROSTER_REPO` = the private admin repository holding `roster.json` + - `PROVISIONING_COHORT_ID` = the cohort that new enrollees are added to 5. Provide `PRIVATE_STUDENT_DATA_TOKEN`, a token that can check out and push to the admin roster repository. +6. Verify credentials before relying on the schedule: run the + **Provisioning Credentials Health Check** workflow (or + `node .github/scripts/provisioning/provision-cli.js --check-auth` locally). It mints + a token and confirms the template repository is reachable, without making changes. + The same check runs weekly and opens a `provisioning-alert` issue on failure. ### Phase 1 spike only: Actions bot (`actions-bot`) @@ -113,8 +123,13 @@ a person and is a single point of failure. ## Running a cohort Provisioning is designed to run as a trickle, on registration, rather than as a -big-bang on go-live morning. The scheduled workflow picks up newly registered learners -every 30 minutes; you can also run it on demand. +big-bang on go-live morning. The registration workflow dispatches provisioning the +moment an enrollment lands, a healing sweep runs on a schedule a few times per day, +and you can also run it on demand. Each run first syncs open `[ENROLL-INTAKE]` issues +from the admin repository into `roster.json` (idempotently, into the cohort named by +`PROVISIONING_COHORT_ID`), so enrollments flow to provisioning with no manual roster +edits. Any failure opens or updates a `provisioning-alert` issue in this repository, +which closes automatically on the next successful run. - **Dry run (no changes).** In the Actions tab, run *Provision Learning Rooms* with the `dry_run` input checked. It lists who would be provisioned. diff --git a/fix.html b/fix.html deleted file mode 100644 index 7e5d42eb..00000000 --- a/fix.html +++ /dev/null @@ -1,237 +0,0 @@ - - - - - - Major Platform Upgrade: Hybrid Provisioning - - - - -

Major Platform Upgrade: Hybrid Provisioning & Enhanced Reliability

- -

What Changed

- -

We've completed a significant upgrade to Git Going with GitHub, transitioning from GitHub Classroom to a GitHub-native Hybrid provisioning system. This change improves reliability, removes external dependencies, and streamlines the student experience.

- -

Key Changes

- -

1. Enrollment Process (Student-Facing)

-
    -
  • Before: GitHub Classroom assignment link → accept → automatic repo creation
  • -
  • After: Enrollment form → issue created → confirm acknowledgment → automated provisioning
  • -
  • Why: Fully GitHub-native, no third-party dependency, more transparent automation
  • -
- -

2. System Architecture (Behind the Scenes)

-
    -
  • Removed: All GitHub Classroom infrastructure, seeding scripts, autograder dependencies
  • -
  • Added: GitHub App-based provisioning with fine-grained permissions
  • -
  • Result: Self-healing, idempotent system that can safely re-run without duplicates
  • -
- -

3. Documentation (Across All Sites)

-
    -
  • Updated 15+ markdown files across /docs, learning-room, and admin guides
  • -
  • Removed 37+ files and directories (Classroom scripts, guides, integration docs)
  • -
  • Regenerated HTML on community-access.org/git-going-with-github and lp.csedesigns.com/ggg
  • -
  • Updated registration page (community-access.org/github) with new enrollment flow
  • -
- -

4. Security & Configuration

-
    -
  • Created GitHub App with least-privilege permissions: -
      -
    • Administration: Read and write (create student repos)
    • -
    • Contents: Read and write (seed and heal repo content)
    • -
    • Issues: Read and write (manage enrollment issues)
    • -
    • Metadata: Read-only (mandatory baseline)
    • -
    -
  • -
  • Secrets securely stored (never in code, only in GitHub Actions secrets)
  • -
  • Provisioning driven by deterministic GitHub signals (issue closures, PR keywords, labels)
  • -
- -

What This Means for You

- -

If You're a Student:

-
    -
  1. Enrollment is now 100% GitHub-native—no third-party account, no extra authorization steps
  2. -
  3. Your Learning Room repo is provisioned automatically after you submit the enrollment form
  4. -
  5. All your challenges, issues, and progress tracking happen in your private GitHub repo
  6. -
  7. Facilitators see your work through the same GitHub interface—no separate tools needed
  8. -
- -

If You're a Facilitator:

-
    -
  1. Roster management: Maintain a simple roster.json file in a private admin repo
  2. -
  3. Provisioning: Trigger via Actions tab or 30-minute schedule (fully automated)
  4. -
  5. Progress tracking: Monitor via GitHub Issues, Pull Requests, and Discussions
  6. -
  7. No more Classroom setup: Integration guide simplified to one deployment guide
  8. -
- -

If You're Running This Locally or on a Server:

-
    -
  1. Follow HYBRID_DEPLOYMENT_GUIDE.md Phases 1–4
  2. -
  3. Phase 1: Prepare admin roster repo with roster.json
  4. -
  5. Phase 2: Create GitHub App (done ✓)
  6. -
  7. Phase 3: Configure environment variables and secrets (done ✓)
  8. -
  9. Phase 4: Run smoke test with a test learner account
  10. -
  11. Phase 5 (optional): Deploy Flask companion for web-based enrollment form
  12. -
- -
- -

Enhancements & Reliability

- -

Why This Is More Reliable

- -

1. No External Dependencies

-
    -
  • Before: Depended on GitHub Classroom (could go down, change, or discontinue)
  • -
  • After: 100% GitHub-native—uses only GitHub API and Actions
  • -
  • Impact: Zero risk of third-party service disruption
  • -
- -

2. Idempotent Design

-
    -
  • Provisioning is safe to re-run. Running the workflow twice creates: -
      -
    • First run: "created" (student repo provisioned)
    • -
    • Second run: "already-exists" (no changes, no duplicates)
    • -
    -
  • -
  • Impact: No data loss, no accidental overwrites, safe manual retries
  • -
- -

3. Least-Privilege Security

-
    -
  • GitHub App uses fine-grained permissions (not personal tokens or org-wide keys)
  • -
  • Secrets stored in GitHub, never in code (PEM key only in Actions secrets)
  • -
  • Credentials minted on-demand and never persisted
  • -
  • Impact: Compliance-ready, audit-trail via GitHub, easy key rotation
  • -
- -

4. Deterministic Signals

-
    -
  • Progress tracked by GitHub actions (issue closed, PR merged, label applied)
  • -
  • No hidden state in external databases
  • -
  • All history visible in GitHub commit/issue timeline
  • -
  • Impact: Transparent, debuggable, recoverable from git history
  • -
- -

5. Self-Healing

-
    -
  • If a student is missed (network hiccup, bot failure), re-running provisions them
  • -
  • If a repo is accidentally deleted, re-run the same workflow to recreate it
  • -
  • Roster is the source of truth, provisioning log is the audit trail
  • -
  • Impact: Administrator can fix issues without manual intervention
  • -
- -

What Stays the Same

- -
    -
  • Your Learning Room repository structure — Same challenges, same workflow automation
  • -
  • Challenge progression system — Challenges unlock in sequence as before
  • -
  • Gandalf bot feedback — Still validates PRs and provides educational feedback
  • -
  • All course content — Same 16 core + 5 bonus challenges, same podcasts and guides
  • -
  • Accessibility standards — Keyboard-only and screen-reader compatible (unchanged)
  • -
- -

Removed & Replaced

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
What WasReasonWhat's New
GitHub Classroom integrationService going away, external dependencyGitHub App provisioning
scripts/classroom/Classroom-specific utilities no longer neededscripts/provisioning/
admin/classroom/Classroom admin guidesadmin/HYBRID_DEPLOYMENT_GUIDE.md
Classroom autograder safeguardsGitHub Classroom no longer usedNative GitHub Actions workflows
classroom-enrollment.yml issue templateReplaced by form-based enrollmentstart-here-roadmap.yml
- -

Dates & Availability

- -
    -
  • Documentation Updated: June 2, 2026
  • -
  • Public Sites Deployed: June 2, 2026 -
      -
    • community-access.org/git-going-with-github
    • -
    • lp.csedesigns.com/ggg
    • -
    -
  • -
  • Enrollment Form: Ready now at community-access.org/github
  • -
  • GitHub App: Configured and ready for production use
  • -
- -

For Questions or Issues

- - - -

Thank You

- -

This transition strengthens the workshop by removing external dependencies and making the system more transparent, auditable, and resilient. Your learning experience remains our top priority.

- -

Welcome to the new Hybrid provisioning system.

- -
- -

Last updated: June 2, 2026
-Version: 1.0 (Hybrid Provisioning Release)

- -
- - - - diff --git a/fix.md b/fix.md deleted file mode 100644 index 31b253fb..00000000 --- a/fix.md +++ /dev/null @@ -1,144 +0,0 @@ -# Major Platform Upgrade: Hybrid Provisioning & Enhanced Reliability - -## What Changed - -We've completed a significant upgrade to **Git Going with GitHub**, transitioning from GitHub Classroom to a **GitHub-native Hybrid provisioning system**. This change improves reliability, removes external dependencies, and streamlines the student experience. - -### Key Changes - -#### 1. **Enrollment Process** (Student-Facing) -- **Before:** GitHub Classroom assignment link → accept → automatic repo creation -- **After:** Enrollment form → issue created → confirm acknowledgment → automated provisioning -- **Why:** Fully GitHub-native, no third-party dependency, more transparent automation - -#### 2. **System Architecture** (Behind the Scenes) -- **Removed:** All GitHub Classroom infrastructure, seeding scripts, autograder dependencies -- **Added:** GitHub App-based provisioning with fine-grained permissions -- **Result:** Self-healing, idempotent system that can safely re-run without duplicates - -#### 3. **Documentation** (Across All Sites) -- Updated 15+ markdown files across `/docs`, learning-room, and admin guides -- Removed 37+ files and directories (Classroom scripts, guides, integration docs) -- Regenerated HTML on community-access.org/git-going-with-github and lp.csedesigns.com/ggg -- Updated registration page (community-access.org/github) with new enrollment flow - -#### 4. **Security & Configuration** -- Created GitHub App with least-privilege permissions: - - Administration: Read and write (create student repos) - - Contents: Read and write (seed and heal repo content) - - Issues: Read and write (manage enrollment issues) - - Metadata: Read-only (mandatory baseline) -- Secrets securely stored (never in code, only in GitHub Actions secrets) -- Provisioning driven by deterministic GitHub signals (issue closures, PR keywords, labels) - -### What This Means for You - -#### **If You're a Student:** -1. Enrollment is now **100% GitHub-native**—no third-party account, no extra authorization steps -2. Your Learning Room repo is provisioned **automatically** after you submit the enrollment form -3. All your challenges, issues, and progress tracking happen in your private GitHub repo -4. Facilitators see your work through the same GitHub interface—no separate tools needed - -#### **If You're a Facilitator:** -1. **Roster management:** Maintain a simple `roster.json` file in a private admin repo -2. **Provisioning:** Trigger via Actions tab or 30-minute schedule (fully automated) -3. **Progress tracking:** Monitor via GitHub Issues, Pull Requests, and Discussions -4. **No more Classroom setup:** Integration guide simplified to one deployment guide - -#### **If You're Running This Locally or on a Server:** -1. Follow [HYBRID_DEPLOYMENT_GUIDE.md](admin/HYBRID_DEPLOYMENT_GUIDE.md) Phases 1–4 -2. **Phase 1:** Prepare admin roster repo with `roster.json` -3. **Phase 2:** Create GitHub App (done ✅) -4. **Phase 3:** Configure environment variables and secrets (done ✅) -5. **Phase 4:** Run smoke test with a test learner account -6. **Phase 5 (optional):** Deploy Flask companion for web-based enrollment form - ---- - -## Enhancements & Reliability - -### **Why This Is More Reliable** - -#### 1. **No External Dependencies** -- **Before:** Depended on GitHub Classroom (could go down, change, or discontinue) -- **After:** 100% GitHub-native—uses only GitHub API and Actions -- **Impact:** Zero risk of third-party service disruption - -#### 2. **Idempotent Design** -- **Provisioning is safe to re-run.** Running the workflow twice creates: - - First run: "created" (student repo provisioned) - - Second run: "already-exists" (no changes, no duplicates) -- **Impact:** No data loss, no accidental overwrites, safe manual retries - -#### 3. **Least-Privilege Security** -- **GitHub App uses fine-grained permissions** (not personal tokens or org-wide keys) -- **Secrets stored in GitHub, never in code** (PEM key only in Actions secrets) -- **Credentials minted on-demand and never persisted** -- **Impact:** Compliance-ready, audit-trail via GitHub, easy key rotation - -#### 4. **Deterministic Signals** -- Progress tracked by GitHub actions (issue closed, PR merged, label applied) -- No hidden state in external databases -- All history visible in GitHub commit/issue timeline -- **Impact:** Transparent, debuggable, recoverable from git history - -#### 5. **Self-Healing** -- If a student is missed (network hiccup, bot failure), re-running provisions them -- If a repo is accidentally deleted, re-run the same workflow to recreate it -- Roster is the source of truth, provisioning log is the audit trail -- **Impact:** Administrator can fix issues without manual intervention - ---- - -## What Stays the Same - -✅ **Your Learning Room repository structure** — Same challenges, same workflow automation -✅ **Challenge progression system** — Challenges unlock in sequence as before -✅ **Gandalf bot feedback** — Still validates PRs and provides educational feedback -✅ **All course content** — Same 16 core + 5 bonus challenges, same podcasts and guides -✅ **Accessibility standards** — Keyboard-only and screen-reader compatible (unchanged) - ---- - -## Removed & Replaced - -| What Was | Reason | What's New | -|----------|--------|-----------| -| GitHub Classroom integration | Service going away, external dependency | GitHub App provisioning | -| `scripts/classroom/` | Classroom-specific utilities no longer needed | `scripts/provisioning/` | -| `admin/classroom/` | Classroom admin guides | `admin/HYBRID_DEPLOYMENT_GUIDE.md` | -| Classroom autograder safeguards | GitHub Classroom no longer used | Native GitHub Actions workflows | -| `classroom-enrollment.yml` issue template | Replaced by form-based enrollment | `start-here-roadmap.yml` | - ---- - -## Dates & Availability - -- **Documentation Updated:** June 2, 2026 -- **Public Sites Deployed:** June 2, 2026 - - community-access.org/git-going-with-github - - lp.csedesigns.com/ggg -- **Enrollment Form:** Ready now at community-access.org/github -- **GitHub App:** Configured and ready for production use - ---- - -## For Questions or Issues - -- **Student Support:** [Community Access Support Hub](https://github.com/Community-Access/support) -- **Facilitator Setup:** See [HYBRID_DEPLOYMENT_GUIDE.md](admin/HYBRID_DEPLOYMENT_GUIDE.md) -- **Technical Details:** [OWNED_PROVISIONING.md](admin/OWNED_PROVISIONING.md), [SPEC.md](admin/SPEC.md) -- **GitHub Repository:** [Community-Access/git-going-with-github](https://github.com/Community-Access/git-going-with-github) - ---- - -## Thank You - -This transition strengthens the workshop by removing external dependencies and making the system more transparent, auditable, and resilient. **Your learning experience remains our top priority.** - -Welcome to the new Hybrid provisioning system. - ---- - -*Last updated: June 2, 2026* -*Version: 1.0 (Hybrid Provisioning Release)* diff --git a/html/CLAUDE.html b/html/CLAUDE.html new file mode 100644 index 00000000..bbb1d5cc --- /dev/null +++ b/html/CLAUDE.html @@ -0,0 +1,286 @@ + + + + + + + CLAUDE.md - GIT Going with GitHub + + + + + + + + +
+ +
+ + + +

CLAUDE.md

+

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

+

What This Repository Is

+

"GIT Going with GitHub" - an accessible, screen-reader-first workshop teaching blind and low-vision users to contribute to open source on GitHub. This is primarily a documentation project (Markdown in docs/) with three supporting systems: an HTML build pipeline, a podcast audio pipeline, and GitHub Actions automation that provisions and grades per-student "Learning Room" repositories.

+

Commands

+
npm run build:html          # Regenerate html/ from Markdown (also regenerates work.md)
+npm run watch:html          # Auto-rebuild HTML on Markdown changes
+npm run clean               # Delete html/
+
+npm run test:automation     # node --test .github/scripts/__tests__/*.test.js
+npm run test:provisioning   # node --test .github/scripts/provisioning/__tests__/*.test.js
+npm run test:llm-execution  # Podcast LLM job execution tests
+
+# Single test file:
+node --test .github/scripts/provisioning/__tests__/roster.test.js
+
+npm run validate:podcasts               # Podcast catalog + listening-order validation
+npm run validate:authoritative-sources  # Checks "Authoritative Sources" sections in docs
+

Tests use the built-in Node test runner (Node >= 20), no test framework dependency. The podcast audio/TTS steps (build:podcast-audio, metadata, inventory) require Python and ffmpeg and run locally; treat them as out of scope unless asked.

+

Critical Workflow: HTML Is Committed, Not CI-Built

+

There is no CI build for the docs. After editing any .md file, run npm run build:html and commit both the Markdown source and the regenerated html/ output in the same commit. docs/*.md -> html/docs/*.html, README.md -> html/index.html, plus learning-room/ and root-level docs. The build template and CSS live in scripts/build-html.js.

+

Architecture

+
    +
  • docs/ - The curriculum: chapters 00-22 plus appendix-*.md, CHALLENGES.md (challenge hub), and solutions/ (reference solutions). Note: appendix display letters (A-Z in README tables) do NOT match appendix filenames - they were renumbered; go by the README tables, not the filename letter.
  • +
  • learning-room/ - Template repository copied into each student's private repo during provisioning. Its own .github/ holds the student-facing automation (challenge issue templates, validation bot, progression workflows). scripts/generate-work-md.js derives work.md from these challenge templates.
  • +
  • .github/scripts/provisioning/ - "Hybrid provisioning": the GitHub-native replacement for GitHub Classroom (which was removed entirely). provision-core.js is pure orchestration - it takes an injected client (github-client.js), never touches the network or secrets directly, and returns an updated roster plus a log. Tests run it against a fake client. github-app-auth.js handles GitHub App authentication; roster.js and roster.schema.json define the student roster; provision-cli.js is the entry point; the workflow is provision-learning-rooms.yml. The algorithm (serial, backoff, idempotent) is specified in SPEC.md section 7.2b - keep code and spec in sync.
  • +
  • .github/scripts/ (top level) - PR validation and challenge-progression bots. Some are legacy from the old shared-repository model; see .github/README.md for which workflows are active vs. legacy.
  • +
  • podcasts/ - Audio companion pipeline: build-bundles.js -> per-episode source bundles -> scripts -> local ONNX TTS (Kokoro default, Piper fallback) -> generate-site.js builds the player page and RSS feed. config/listening-order.json is the canonical episode order shared by JS (lib/listening-plan.js) and Python (listening_plan.py) resolvers. Audio files are gitignored and hosted on GitHub Releases.
  • +
  • scripts/ - Repo-level build tooling (HTML, ePub, BRF, diagram SVGs, source validation).
  • +
+

Root-level tmp-* files, work.md/work.html, and golden.md/golden.html are generated or scratch artifacts - do not hand-edit them.

+

Conventions

+
    +
  • Never use emojis anywhere: docs, code, comments, commit messages (from .github/copilot-instructions.md).
  • +
  • All documentation is written screen-reader-first: semantic headings, descriptive link text, keyboard-only instructions, no meaning conveyed by visuals alone.
  • +
  • Docs end with an "Authoritative Sources" section plus a "Section-Level Source Map"; npm run validate:authoritative-sources checks these. Preserve them when editing docs.
  • +
  • Commit messages in this repo follow conventional-commit style (docs:, build:, refactor:, ...).
  • +
  • License is CC BY 4.0.
  • +
+
+ +
+
+ + + + + diff --git a/html/DEPLOYMENT_ASSESSMENT.html b/html/DEPLOYMENT_ASSESSMENT.html new file mode 100644 index 00000000..8b3f1789 --- /dev/null +++ b/html/DEPLOYMENT_ASSESSMENT.html @@ -0,0 +1,557 @@ + + + + + + + Deployment Assessment: Hybrid Provisioning System - GIT Going with GitHub + + + + + + + + +
+ +
+ + + +

Deployment Assessment: Hybrid Provisioning System

+

Date: June 2, 2026
Status: READY FOR PRODUCTION (with caveat below)

+

Summary

+

The Hybrid provisioning system is architecturally sound, hardened, and production-ready. However, the end-to-end workflow has not yet been tested with real student enrollment. Recommendation: Run Phase 4 smoke test before admitting first cohort.

+
+

Hardening Assessment by Component

+

OK: GitHub App Configuration

+
    +
  • Status: HARDENED
  • +
  • Why: Fine-grained permissions (least privilege), secrets stored in GitHub Actions only, PEM key never in code
  • +
  • Risk: None identified
  • +
  • Action: READY
  • +
+

OK: Secrets & Variables Management

+
    +
  • Status: HARDENED
  • +
  • Why: All three secrets (App ID, Installation ID, PEM) stored securely in GitHub repository Actions secrets; variables configured correctly
  • +
  • Risk: None identified
  • +
  • Action: READY
  • +
+

OK: Infrastructure Code (Provisioning Scripts)

+
    +
  • Status: HARDENED
  • +
  • Why:
      +
    • Idempotent: Re-running is safe (already-exists vs. created state)
    • +
    • Self-healing: Missing students auto-provisioned on next run
    • +
    • Deterministic: Uses GitHub API signals, not external state
    • +
    • Auditable: All history in provisioning-log.json and git
    • +
    +
  • +
  • Risk: None identified
  • +
  • Action: READY
  • +
+

OK: Documentation Updates

+
    +
  • Status: COMPLETE
  • +
  • What: 15+ files updated, 37+ files deleted, HTML regenerated on both sites
  • +
  • Coverage: Student-facing, facilitator, operator all covered
  • +
  • Risk: None identified
  • +
  • Action: READY
  • +
+

OK: Public Site Deployments

+
    +
  • Status: COMPLETE
  • +
  • Coverage:
      +
    • community-access.org/git-going-with-github (auto via GitHub Pages)
    • +
    • lp.csedesigns.com/ggg (manual via rsync + Caddy reload)
    • +
    +
  • +
  • Verification: Both sites live and tested (404s work, navigation intact)
  • +
  • Risk: None identified
  • +
  • Action: READY
  • +
+

Warning: End-to-End Student Workflow (NOT YET TESTED)

+
    +
  • Status: UNTESTED
  • +
  • What's missing:
      +
    1. Admin roster repo creation
    2. +
    3. Smoke test with test student account
    4. +
    5. Verification that provision workflow runs end-to-end
    6. +
    7. Verification that invitation email sent to test account
    8. +
    9. Verification that student can accept invite and access repo
    10. +
    +
  • +
  • Risk: Medium-logical gaps possible, though architecture is solid
  • +
  • Action: REQUIRED-Run Phase 4 before admitting real students
  • +
+
+

What Has Been Tested

+
    +
  • OK: Automation tests: 78/78 passing (was 98, reduced after removing Classroom tests)
  • +
  • OK: Provisioning tests: 55/55 passing
  • +
  • OK: HTML regeneration: 393 markdown files -> HTML
  • +
  • OK: Site deployments: Both production sites live
  • +
  • OK: GitHub App creation: Properly configured with correct permissions
  • +
  • OK: Secrets storage: All three values stored securely
  • +
+
+

What Has NOT Been Tested

+
    +
  • Missing: End-to-end provisioning flow with a real test student
      +
    • Enrollment form submission
    • +
    • Issue creation in test account
    • +
    • Automation comment with learning room link
    • +
    • Provisioning workflow execution
    • +
    • Private repo creation and invitation
    • +
    • Student repo readiness for challenges
    • +
    +
  • +
+
+

Failsafe Mechanisms

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MechanismHow It WorksWhen It Helps
IdempotenceRe-running workflow is safe; state tracked in roster.jsonIf automation fails, re-run fixes it
Self-healingMissing students auto-provisioned on next runIf student missed by error, they're caught on retry
Audit Trailprovisioning-log.json records every actionTroubleshooting, rollback, verification
Source of Truthroster.json is canonical; all state derives from itRepo can be recreated, roster is immutable
Least PrivilegeGitHub App has minimal permissionsLimits blast radius if credentials compromised
Secrets in ActionsPEM never stored in code or logsPrevents accidental exposure
Deterministic SignalsProgress tracked via GitHub eventsNo hidden external dependencies
+
+

Recommendations Before Go-Live

+

MUST DO (Blocking)

+
    +
  1. Run Phase 4 Smoke Test

    +
      +
    • Create test admin roster repo
    • +
    • Add one test learner (e.g., a throwaway GitHub account you control)
    • +
    • Trigger provisioning workflow with dry-run first
    • +
    • Trigger provisioning workflow without dry-run
    • +
    • Verify:
        +
      • Private repo created
      • +
      • Invitation sent to test account
      • +
      • Roster entry updated to "provisioned"
      • +
      • provisioning-log.json records "created"
      • +
      +
    • +
    • Run again; verify log records "already-exists"
    • +
    • Test account accepts invite; repo is ready for challenges
    • +
    +
  2. +
  3. Verify End-to-End Student Flow

    +
      +
    • Submit enrollment form as test student
    • +
    • Verify issue created in your account
    • +
    • Reply ack to issue
    • +
    • Verify automation comment posts learning room link
    • +
    • Verify you receive GitHub invitation
    • +
    • Accept invitation; verify repo has challenges
    • +
    +
  4. +
+ +
    +
  1. Verify Flask Companion (Optional)

    +
      +
    • If deploying companion for web enrollment, test locally first
    • +
    • See Phase 5 of deployment guide
    • +
    +
  2. +
  3. Document Rollback Plan

    +
      +
    • If provisioning fails, how to revert?
    • +
    • If student repo corrupted, how to re-provision?
    • +
    • Document in runbook
    • +
    +
  4. +
  5. Test with Multiple Students

    +
      +
    • Once Phase 4 passes, add 3-5 test students to roster
    • +
    • Verify all get provisioned correctly
    • +
    • Verify no duplicates or conflicts
    • +
    +
  6. +
+

NICE TO HAVE (After launch)

+
    +
  1. Monitor first week of production:

    +
      +
    • Track provisioning success rate
    • +
    • Log any failures to support channel
    • +
    • Verify no duplicate repos
    • +
    • Verify all students receive invitations
    • +
    +
  2. +
  3. Set up alerts:

    +
      +
    • GitHub Actions workflow failures
    • +
    • Provisioning-log.json anomalies
    • +
    • Missing students in roster
    • +
    +
  4. +
+
+

Failsafety Verdict

+

System is FAILSAFE for:

+
    +
  • Re-running provisioning: Safe, idempotent, no duplicates
  • +
  • Recovering from failures: Re-run workflow, roster is source of truth
  • +
  • Credential compromise: GitHub App scope limited, PEM in Actions only
  • +
  • Data loss: Audit trail in provisioning-log.json, roster can be rebuilt from git history
  • +
  • Classroom service issues: No longer dependent on GitHub Classroom
  • +
+

System NEEDS TESTING for:

+
    +
  • Real student enrollment: End-to-end flow not yet validated
  • +
  • Scale: Tested with unit tests (78 automation, 55 provisioning), not with 50+ real students
  • +
  • Edge cases: Duplicate submissions, roster conflicts, network timeouts during provisioning
  • +
+
+

Deployment Timeline Recommendation

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PhaseWhenWhoWhat
VerificationNOWYouRun Phase 4 smoke test (1-2 hours)
DocumentationAfter smoke testYouDocument any adjustments, update runbook
Soft LaunchDay after verificationFacilitatorsAdmit 1-3 test students, monitor
Full LaunchDay after soft launchFacilitatorsAdmit full cohort
+
+

Conclusion

+

The system is architecturally hardened and production-ready. All infrastructure code, secrets management, documentation, and deployments are in place and verified. The one gap is end-to-end testing with a real student enrollment flow, which is low risk but high confidence to pass, given the robustness of the underlying design.

+

Recommendation: Run Phase 4 smoke test, then launch with confidence.

+
+

Assessment completed June 2, 2026 +System: Hybrid Provisioning v1.0

+
+

Correction (July 2, 2026)

+

This assessment's verdict did not hold. The Phase 4 smoke test listed above as MUST DO was never completed: the provisioning GitHub App was created but never installed on the Community-Access organization, so every non-dry-run provisioning attempt from June 2 onward failed while minting an installation token (HTTP 404), and two enrolled learners remained unprovisioned for a month with no alert.

+

Lessons now encoded in the workflow and runbook:

+
    +
  • An untested MUST DO item means the status is NOT READY. The smoke test is the release gate; no schedule is enabled until a real (non-dry-run) run has succeeded once.
  • +
  • Every READY claim in an assessment must cite evidence (a green run ID), especially for credentials.
  • +
  • Scheduled workflows must alert on failure. The provisioning workflow now opens a watchdog issue on any failure, and a weekly credentials health check catches drift between enrollments.
  • +
+

See REVIEW-2026-07-02.md for the full incident review.

+

Authoritative Sources

+

Use these official references when you need the current source of truth for facts in this chapter.

+ +

Section-Level Source Map

+

Use this map to verify facts for each major section in this file.

+ +
+ +
+
+ + + + + diff --git a/html/REVIEW-2026-07-02.html b/html/REVIEW-2026-07-02.html new file mode 100644 index 00000000..7d66fedf --- /dev/null +++ b/html/REVIEW-2026-07-02.html @@ -0,0 +1,357 @@ + + + + + + + Operations Review: Provisioning Failures and Workshop Model Robustness - GIT Going with GitHub + + + + + + + + +
+ +
+ + + +

Operations Review: Provisioning Failures and Workshop Model Robustness

+

Date: July 2, 2026 +Scope: GitHub Actions failure analysis, provisioning subsystem, registration pipeline, repo hygiene.

+

Implementation status (July 2, 2026): everything in sections 3, 4.1-4.4, and 6 that is +code has been implemented (installation-ID auto-discovery, watchdog alert issue, +work-free early exit, event-driven provisioning dispatch, weekly credentials health +check, preflight assertions, intake-to-roster sync, Classroom link removal, [REGISTER] +private intake plus redaction, authoritative-sources gate fixes, scratch cleanup). +The remaining manual step is section 2: install the GitHub App on the +Community-Access organization. Template drift (4.5) is still open.

+

1. Headline Finding: The Provisioning GitHub App Is Not Installed on the Org

+

The "Provision Learning Rooms" workflow has failed on every real run since it was deployed. Verified numbers from the GitHub API:

+
    +
  • 280 total runs of provision-learning-rooms.yml.
  • +
  • Exactly 1 success ever (June 2, 2026, 20:16 UTC) - and that run was a dry run, which skips authentication entirely.
  • +
  • Every non-dry run, including roughly 12 scheduled runs per day for the past 30 days, fails with: +Failed to mint installation token (HTTP 404)
  • +
+

Root cause, confirmed directly: GET /orgs/Community-Access/installations shows exactly one GitHub App installed on the org - digitalocean (app id 64711). The provisioning App was created (App ID and PEM are stored as secrets) but was never installed on the Community-Access organization. There is no installation to mint a token against, so the token mint returns 404 no matter what PROVISIONING_APP_INSTALLATION_ID contains.

+

Consequences, also confirmed:

+
    +
  • The private roster (git-going-with-github-administration/roster.json) has 2 learners in provision_state: pending - unchanged since June 2. Two registered people have been waiting a month for learning rooms that were never created.
  • +
  • The admin repo has had no roster commits since June 2 because the workflow dies before the commit step.
  • +
  • This is precisely the failure class SPEC.md's golden principle warns about ("never let a single point of failure hold a cohort hostage") and violates the section 7.2b invariant "any failure visible to a human before a learner notices." The watchdog the spec calls for does not exist.
  • +
+

Note on the local working-tree changes: the uncommitted edits to github-app-auth.js (normalize URL-form installation IDs, better 404 hint) are good hardening and all 58 provisioning tests pass, but they will not fix production. No installation ID is valid until the App is installed.

+

2. Immediate Fix (Operator Steps, ~15 Minutes)

+
    +
  1. Install the App: GitHub -> Organization settings -> Developer settings -> GitHub Apps -> your provisioning App -> Install App -> Community-Access. Because the App must create new repositories, install it org-wide (repository creation cannot be granted through a selected-repositories installation).
  2. +
  3. Capture the installation ID from the post-install URL (.../settings/installations/<id>) or from GET /app/installations using an App JWT.
  4. +
  5. Update the PROVISIONING_APP_INSTALLATION_ID secret (it lives in the provisioning environment, not repo-level secrets).
  6. +
  7. Run the workflow manually with dry_run: true (expect: "2 learner(s) would be provisioned"), then run it for real.
  8. +
  9. Verify: two new learning-room-* repos, invitations sent, roster entries flipped to provisioned, provisioning-log.json committed to the admin repo.
  10. +
+

3. Make the Failure Class Impossible: Code Changes

+

These are ordered by payoff.

+

3.1 Auto-discover the installation ID

+

Stop storing the installation ID as a secret at all. With an App JWT you can call GET /app/installations and select the installation whose account matches PROVISIONING_STUDENT_OWNER. Add this as the fallback (or the default) in github-app-auth.js / provision-cli.js. This removes an entire class of copy-paste misconfiguration and survives App re-installation (installation IDs change when an App is uninstalled and reinstalled).

+

Alternative for the workflow path: the official actions/create-github-app-token action needs only app-id and private-key and resolves the installation from the repo owner automatically. It keeps the zero-npm-dependency property of the repo since it runs as an action, and the CLI keeps the hand-rolled path for local use.

+

3.2 Alert on failure - implement the spec's watchdog

+

Thirty days of silent failure proves the need. Add a final if: failure() step to provision-learning-rooms.yml that creates or updates a pinned, labeled issue (for example provisioning-broken) in this repo, including the error line from the log. Close it automatically on the next success. Requires issues: write on this workflow only.

+

3.3 Skip work-free runs

+

Most of the 280 failed runs had zero learners to provision. Read the roster first; if there are no pending/failed entries, exit success before minting a token. Fewer runs, less noise, and credential problems surface only when they matter - paired with 3.5 so they still surface promptly.

+

3.4 Event-driven provisioning instead of 30-minute polling

+

The registration workflow already fires on issues: opened. Have the enrollment path send a repository_dispatch (or workflow_dispatch) to trigger provisioning immediately, and relax the cron to a few times per day as a healing sweep. Students get rooms in minutes instead of up to 30 (in practice, GitHub throttles the 30-minute cron to roughly every 2 hours anyway), and scheduled load drops by 90 percent.

+

3.5 Credential health check

+

A small weekly job that mints a token and makes one read call (GET /app), with the same failure-alert step. Catches expired keys, uninstalled Apps, and permission drift before a learner does.

+

3.6 Preflight assertions

+

Before iterating the roster, provision-cli.js should verify the minted token can read LEARNING_ROOM_TEMPLATE_REPO. Fail with one clear message instead of per-learner errors.

+

4. Pipeline Gaps in the Workshop Model

+

4.1 Registration does not reach the provisioning roster

+

The chain today: [ENROLL] issue -> intake issue in the private admin repo -> (manual, undocumented step) -> roster.json -> provisioning. Nothing automates the middle hop. The 2 pending learners suggest roster entries were hand-written once on June 2 and never again. Recommendation: a workflow in the admin repo that converts intake issues into pending roster entries (idempotent on github_handle+cohort), or teach provision-cli.js to read intake issues directly. Until then, document the manual step in the runbook so it is at least a known duty.

+ +

CLASSROOM_DAY1_ASSIGNMENT_URL / CLASSROOM_DAY2_ASSIGNMENT_URL repo variables still point at classroom.github.com/a/... and the registration welcome comment still sends people there - but Classroom was removed from the program in the June 2 transition. Anyone registering today gets a broken onboarding path. Replace these variables and the welcome text with the Hybrid flow (enroll issue -> learning room invitation), or clear the vars so the fallback text ("a facilitator will share your link") is used.

+

4.3 PII in public issue bodies

+

The [ENROLL] path redacts the public issue body after capturing intake privately - good. The [REGISTER] path does not: full name and email remain in public issue bodies indefinitely. Apply the same redact-after-capture pattern to [REGISTER], and consider a backfill pass over historical registration issues.

+

4.4 Two overlapping intake paths

+

[REGISTER] (waitlist/welcome/CSV) and [ENROLL] (intake/provisioning) coexist with different field schemas and different privacy behavior. For the self-paced era, collapse to one form that captures minimal public data (GitHub handle only), stores the rest privately, and feeds the roster automatically.

+

4.5 Template drift risk

+

The workshop repo carries learning-room/ while provisioning seeds from the separate Community-Access/learning-room-template repo (last pushed May 22). Two sources of truth will drift. Either add a sync workflow (this repo -> template repo on change to learning-room/**) or delete one copy and point everything at the survivor.

+

5. Process Finding: The Assessment Predicted This

+

DEPLOYMENT_ASSESSMENT.md (June 2) declared the system "READY FOR PRODUCTION" while flagging, in its own MUST-DO section, that the end-to-end smoke test had not been run. The smoke test would have failed in under a minute and pointed at the missing installation. The lesson to encode in the runbook and QA gate: an untested MUST-DO item means the status is NOT READY - the smoke test is the gate, and no schedule gets enabled until a real (non-dry-run) invocation has succeeded once. The assessment also states "Secrets & Variables: HARDENED, Risk: None identified" for a credential set that had never authenticated successfully; future assessments should require evidence (a green run ID) next to every READY claim.

+

6. Repo Hygiene and Smaller Findings

+
    +
  • validate-authoritative-sources fails at HEAD: DEPLOYMENT_ASSESSMENT.md and fix.md are missing the required "Authoritative Sources" and "Section-Level Source Map" sections. Every docs push will keep failing this gate until fixed (add sections, or exclude assessment/scratch files in scripts/validate-authoritative-sources.js).
  • +
  • DEPLOYMENT_ASSESSMENT.md is full of emoji, violating the repo's own no-emoji rule in .github/copilot-instructions.md.
  • +
  • Root clutter: tmp-*.csv, tmp-*.log, tmp-*.ps1, fix.md/fix.html, key.txt (verify this is not a credential; if it ever held one, rotate it), and two learning-room-e2e-*.json files sit at repo root. Move to an untracked scratch area or delete; add tmp-* to .gitignore.
  • +
  • PROVISIONING_TOKEN (actions-bot spike mode) is still wired into the production workflow env, empty. The spec calls the PAT path "throwaway." Remove it from the workflow to shrink the config surface.
  • +
  • Actions runners warn that actions/checkout@v4 and actions/setup-node@v4 target deprecated Node 20; bump to the current majors during the next maintenance pass.
  • +
  • learning-room-pr-bot shows occasional action_required conclusions (first-time fork contributors awaiting approval) - expected, but worth a facilitator habit of clearing the queue.
  • +
+

7. Suggested Order of Work

+
    +
  1. Install the App, fix the secret, provision the 2 waiting learners (section 2). Everything else is secondary to unblocking real people.
  2. +
  3. Commit the local github-app-auth.js hardening plus installation-ID auto-discovery (3.1).
  4. +
  5. Add failure alerting to the provisioning workflow (3.2) - small diff, permanent visibility.
  6. +
  7. Fix the dead Classroom links in registration (4.2) and the [REGISTER] PII redaction (4.3).
  8. +
  9. Fix the validate-authoritative-sources gate (section 6) so CI is green at HEAD.
  10. +
  11. Event-driven provisioning + registration-to-roster automation (3.4, 4.1) - this is the "robust model": one intake form, automatic roster append, provision-on-registration, healing sweep cron, watchdog issue on any failure, weekly credential health check.
  12. +
+

Authoritative Sources

+

Use these official references when you need the current source of truth for facts in this chapter.

+ +

Section-Level Source Map

+

Use this map to verify facts for each major section in this file.

+ +
+ +
+
+ + + + + diff --git a/html/SPEC.html b/html/SPEC.html index cb2156db..35f09b53 100644 --- a/html/SPEC.html +++ b/html/SPEC.html @@ -633,9 +633,10 @@

7.2a GitHub App permission set

App configuration rules:

    -
  • Install the App only on the Community-Access organization, scoped to the template and student repositories.
  • -
  • Store PROVISIONING_APP_ID and PROVISIONING_APP_PRIVATE_KEY in GitHub Secrets; never in code or public repos.
  • +
  • Install the App on the Community-Access organization with all-repositories access (creating student repositories requires an organization-wide installation). An App that exists but is not installed fails every token mint with HTTP 404.
  • +
  • Store PROVISIONING_APP_ID and PROVISIONING_APP_PRIVATE_KEY in GitHub Secrets; never in code or public repos. PROVISIONING_APP_INSTALLATION_ID is optional: when unset, provisioning discovers the installation from the App at runtime, which removes a copy-paste failure mode and survives re-installation.
  • Mint a short-lived installation token at the start of each provisioning run; never persist it.
  • +
  • Verify credentials independently of enrollment: a weekly health check mints a token and confirms template access, and any provisioning failure opens a provisioning-alert issue (the watchdog required by section 7.3).
  • Document a private-key rotation procedure and rotate on any suspected exposure.

7.2b Provisioning algorithm (idempotent, serial)

diff --git a/html/admin/HYBRID_DEPLOYMENT_GUIDE.html b/html/admin/HYBRID_DEPLOYMENT_GUIDE.html index 20c5375b..1927f1ec 100644 --- a/html/admin/HYBRID_DEPLOYMENT_GUIDE.html +++ b/html/admin/HYBRID_DEPLOYMENT_GUIDE.html @@ -333,12 +333,20 @@

Phase 2: Create the provisio
  • Create the App, then note the numeric App ID.
  • Generate a private key; a .pem file downloads. Keep it secret.
  • -
  • Install the App on the Community-Access organization, scoped to the template -repository and the student repositories (or all repositories if simpler for now).
  • +
  • Install the App on the Community-Access organization with All repositories +access. Creating new student repositories requires an organization-wide +installation; a selected-repositories installation cannot create repos. This step +is mandatory: an App that exists but is not installed fails every token mint with +HTTP 404, and nothing downstream can work.
  • Open the installation and note the numeric Installation ID (it is in the -installation settings URL).
  • +installation settings URL). Storing it is optional - provisioning discovers it +automatically when the secret is unset. -

    Verification: you now hold three values: App ID, Installation ID, and the PEM key.

    +

    Verification: you now hold three values: App ID, Installation ID (optional), and the +PEM key. Confirm they work before continuing: run the Provisioning Credentials +Health Check workflow, or locally +node .github/scripts/provisioning/provision-cli.js --check-auth. Do not proceed to +Phase 4 until the check passes.

    Phase 3: Configure variables and secrets

    Set these on this repository (Settings, Secrets and variables, Actions). Using an Environment named provisioning is recommended so you can add required reviewers.

    @@ -366,6 +374,10 @@

    Phase 3: Configure variables an ADMIN_ROSTER_REPO Community-Access/glow-admin (your admin repo) + +PROVISIONING_COHORT_ID +Cohort that new enrollees are added to +

    Secrets:

    @@ -381,7 +393,7 @@

    Phase 3: Configure variables an

    - + @@ -402,7 +414,7 @@

    Phase 3: Configure variables an

    Phase 4: Deploy and smoke-test provisioning

    The provisioning workflow ships in this repository at ../.github/workflows/provision-learning-rooms.yml. -Merging this branch deploys it. It runs on a 30-minute schedule and on demand.

    +Merging this branch deploys it. It runs when registration dispatches it, on a healing-sweep schedule, and on demand.

    Smoke test with a single test account before any real cohort:

    1. Add one test learner to the admin roster.json (a throwaway GitHub account you @@ -483,7 +495,7 @@

      Phase 6: Go-live checklist

      Operating a cohort

        -
      • Provision on registration, not big-bang. Let the 30-minute schedule trickle new +
      • Provision on registration, not big-bang. Let the registration dispatch and healing sweep trickle new learners in. This keeps you far below any rate limit regardless of cohort size.
      • Watch the roster and log. failed entries and error log lines are your signal to intervene; re-running provisioning heals pending and failed entries.
      • diff --git a/html/admin/OWNED_PROVISIONING.html b/html/admin/OWNED_PROVISIONING.html index 886bbb99..f43903cf 100644 --- a/html/admin/OWNED_PROVISIONING.html +++ b/html/admin/OWNED_PROVISIONING.html @@ -326,11 +326,15 @@

        Production: GitHub App (github-a
      • Create a GitHub App in the Community-Access organization with only these permissions: Repository administration (write), Contents (write), Metadata (read), and optionally Issues (write). Nothing more.
      • -
      • Install the App on the organization, scoped to the template and student repositories.
      • +
      • Install the App on the organization. Creating student repositories requires an +organization-wide installation (repository creation cannot be granted through a +selected-repositories installation). Do not skip this step: an App that exists but +is not installed fails every token mint with HTTP 404.
      • Generate a private key (PEM). Store these as repository or environment secrets:
        • PROVISIONING_APP_ID
        • PROVISIONING_APP_PRIVATE_KEY (the PEM contents)
        • -
        • PROVISIONING_APP_INSTALLATION_ID
        • +
        • PROVISIONING_APP_INSTALLATION_ID (optional; when unset, provisioning discovers +the installation from the App itself, which also survives re-installation)
      • Set repository variables:
          @@ -338,10 +342,16 @@

          Production: GitHub App (github-a
        • LEARNING_ROOM_TEMPLATE_REPO = Community-Access/learning-room-template
        • PROVISIONING_STUDENT_OWNER = Community-Access
        • ADMIN_ROSTER_REPO = the private admin repository holding roster.json
        • +
        • PROVISIONING_COHORT_ID = the cohort that new enrollees are added to
      • Provide PRIVATE_STUDENT_DATA_TOKEN, a token that can check out and push to the admin roster repository.
      • +
      • Verify credentials before relying on the schedule: run the +Provisioning Credentials Health Check workflow (or +node .github/scripts/provisioning/provision-cli.js --check-auth locally). It mints +a token and confirms the template repository is reachable, without making changes. +The same check runs weekly and opens a provisioning-alert issue on failure.

    Phase 1 spike only: Actions bot (actions-bot)

    For an early validation spike you may use a least-privilege fine-grained PAT in @@ -351,8 +361,13 @@

    Phase 1 spike only: Actions a person and is a single point of failure.

    Running a cohort

    Provisioning is designed to run as a trickle, on registration, rather than as a -big-bang on go-live morning. The scheduled workflow picks up newly registered learners -every 30 minutes; you can also run it on demand.

    +big-bang on go-live morning. The registration workflow dispatches provisioning the +moment an enrollment lands, a healing sweep runs on a schedule a few times per day, +and you can also run it on demand. Each run first syncs open [ENROLL-INTAKE] issues +from the admin repository into roster.json (idempotently, into the cohort named by +PROVISIONING_COHORT_ID), so enrollments flow to provisioning with no manual roster +edits. Any failure opens or updates a provisioning-alert issue in this repository, +which closes automatically on the next successful run.

    • Dry run (no changes). In the Actions tab, run Provision Learning Rooms with the dry_run input checked. It lists who would be provisioned.
    • diff --git a/html/search-index.json b/html/search-index.json index 76ec66f5..b3be3b4c 100644 --- a/html/search-index.json +++ b/html/search-index.json @@ -2111,12 +2111,6 @@ "url": "admin/GITHUB_PROPOSAL.html", "body": "GitHub Learning Curriculum Executive Summary & Proposal to GitHub Project Overview Title: Comprehensive, Accessible GitHub Workshop Curriculum with AI Agent Integration Scope: 17-chapter structured learning path + interactive AI agents + accessibility-first design Audience: Developers, open-source contributors, maintainers, and facilitators Status: Complete curriculum ready for deployment Note: 2 additional agenda files (Day 1 & Day 2) provided for workshop facilitators (not part of learner sequence) Problem Statement Current Gap GitHub is powerful but intimidating for newcomers. Standard onboarding approaches: Assume prior Git/collaboration knowledge Don't explicitly teach soft skills (communication, inclusive review) Often lack accessibility guidance Don't leverage AI to amplify human expertise Many organizations need structured, repeatable, accessible training that: Teaches Git and GitHub concepts sequentially Emphasizes inclusive collaboration practices Includes accessibility requirements from day one Integrates modern AI tools (GitHub Copilot agents) Provides hands-on, guided practice with clear success criteria Impact New developers struggle with PR review culture and GitHub workflows Maintainers lack templates and automation to scale contributions Accessibility advocates have no GitHub-specific guidance Facilitators must recreate training materials from scratch Solution: Complete Learning Curriculum Architecture Three-Part Modular Design: Part 1: Foundation (Day 1 - 7.5 hours) Chapters 0-10 teach GitHub essentials and collaborative workflows. Chapter Duration Focus Hands-On 0 30 min Environment setup Install tools, verify Git 1 1 hr GitHub concepts Web structure, terminology 2 45 min Navigation Explore real repositories 3 30 min The Learning Room Practice repository orientation 4 1 hr Issues Create, comment, label (2 exercises) 5 45 min VS Code Accessibility Editor setup, accessibility features 6 1 hr Pull requests Submit, review, merge (2 exercises) 7 1 hr Merge conflicts Resolve real conflicts 8 30 min Culture & etiquette Respectful collaboration 9 45 min Organization tools Labels, milestones, projects 10 30 min Notifications Manage subscriptions Outcome: Users comfortable with GitHub fundamentals; ready for advanced topics and automation (Day 2). Note: Day 1 and Day 2 agenda files provided separately for facilitators (not counted in learner time). Part 2: VS Code & Development Environment (Day 2 Foundation - 2.5 hours) Chapters 11-13 introduce Git integration, GitHub PR tooling, and GitHub Copilot. Chapter Duration Focus New Concepts 11 45 min Git source control VS Code Git integration 12 30 min GitHub PR extension Review PRs from VS Code 13 45 min GitHub Copilot AI-powered code assistance Outcome: Users comfortable with development environment; ready for advanced workflows. Part 3: Accessibility & Advanced Workflows (Day 2 Advanced - 4 hours) Chapters 14-16 teach accessible code review, issue templates, and AI agent automation. Chapter Duration Focus New Concepts 14 1 hr + 3 exercises Accessible code review Keyboard-only, screen readers, diff navigation (NVDA/JAWS/VoiceOver) 15 1.5 hrs + 4 exercises Issue templates YAML, form fields, WCAG compliance, template design 16 1.5 hrs + 3 exercises Accessibility Agents 55 AI agents across 3 teams, 54+ slash commands, Template Builder wizard AI Agent Integration: 55 Agents across 3 Teams: Accessibility (26), GitHub Workflow (12), Developer Tools (6) - automate auditing, issue triage, PR review, analytics, and more 54+ Slash Commands: Targeted invocations for specific workflows Template Builder: Interactive wizard for guided template creation Outcome: Teams can design accessible workflows and automate GitHub processes with confidence. Curriculum Highlights 1. Accessibility-First Design Screen reader integration: Every exercise tested with NVDA, JAWS, VoiceOver Keyboard-only workflows: Users never require a mouse Plain language: Glossary for all terminology Appendix B: Screen Reader Cheatsheet (NVDA/JAWS/VoiceOver commands) Why: ~15% of population has disabilities; GitHub should be usable by all. 2. Hands-On Exercises 10+ guided exercises across Chapters 4-6, 11-16 (all hands-on chapters) Step-by-step walkthroughs: 300-900 lines per exercise **"What you should see":" Checkpoint validation at each step Troubleshooting: "If not, try this" guidance embedded in every exercise Note on Chapters 5 and 11-13: These VS Code chapters teach through integrated practice rather than standalone exercises - users configure tools, explore features, and practice workflows as they learn. Formal numbered exercises resume at Chapter 14. Example: Exercise A (Ch 15) walks users through: Navigating to template selector Reading template instructions Filling form fields accessibly Previewing submission Submitting issue Verifying success Reflecting on accessibility Why: Users gain confidence; zero ambiguity on success criteria. All" }, - { - "id": "admin/HYBRID_DEPLOYMENT_GUIDE.html", - "title": "Hybrid Deployment Guide", - "url": "admin/HYBRID_DEPLOYMENT_GUIDE.html", - "body": "Hybrid Deployment Guide This is the step-by-step guide to deploying the Hybrid architecture from golden.md and SPEC.md : the owned, GitHub-native provisioning core that replaces GitHub Classroom, plus the optional accessible Flask companion. For the conceptual model and day-to-day operations, see OWNED_PROVISIONING.md . For the legacy Classroom-based flow, see ../classroom/README.md . To review every piece of the Hybrid deliverable from one place, start at HYBRID_REVIEW_INDEX.md . Follow the phases in order. Each phase ends with a verification step. Do not advance until the current phase verifies, exactly as the phased roadmap in golden.md requires. Table of contents What you are deploying Prerequisites Phase 1: Prepare the admin roster repository Phase 2: Create the provisioning GitHub App Phase 3: Configure variables and secrets Phase 4: Deploy and smoke-test provisioning Phase 5: Deploy the optional companion Phase 6: Go-live checklist Operating a cohort Rollback and fallback Reference: configuration surface Authoritative Sources What you are deploying Piece Where it runs Required? Provisioning scripts This repository, .github/scripts/provisioning/ Yes Provisioning workflow GitHub Actions in this repository Yes Roster of record ( roster.json ) A private admin repository Yes Provisioning identity A GitHub App in the Community-Access org Yes (production) Learning Room template Community-Access/learning-room-template Yes (already exists) Flask companion A small host you control (or none) No, optional The critical path is the first five rows. The companion is a convenience at the edges and can be skipped entirely; the issue-form front door and admin-issue dashboard carry the workshop without it. Prerequisites Owner or admin access to the Community-Access GitHub organization. The Learning Room template repository exists and passes template validation ( scripts/classroom/Test-LearningRoomTemplate.ps1 ). A private admin repository you control, to hold the roster and provisioning log. Node.js 20 or newer for local runs and tests. Python 3.12 or newer if you deploy the companion. The gh CLI authenticated, for the convenience commands below. Confirm your toolchain: node --version # v20+ python --version # 3.12+ gh auth status Phase 1: Prepare the admin roster repository The roster of record lives in a private repository so intake data is never public. Create (or choose) a private repository, for example Community-Access/glow-admin . Add a starter roster at its root. Copy the shape from examples/roster.example.json , or start empty: { "version" : 1 , "cohorts" : [ ] , "learners" : [ ] } Commit it as roster.json . The provisioning run will also create and maintain provisioning-log.json next to it. Verification: the file validates locally. node -e "require('./.github/scripts/provisioning/roster').parseRoster(require('fs').readFileSync('roster.json','utf8')); console.log('roster ok')" Phase 2: Create the provisioning GitHub App A GitHub App is the production identity because it is not tied to a human account, mints short-lived tokens, and uses fine-grained least-privilege permissions (SPEC.md section 7.2). Create it once. In the organization, go to Settings, Developer settings, GitHub Apps, New GitHub App. Name it (for example GLOW Provisioning ). Set a homepage URL (the repo is fine). Disable the webhook (this App is called by Actions, not by webhooks). Grant only these repository permissions, nothing more: Administration: Read and write (create student repositories). Contents: Read and write (seed and heal repository content). Metadata: Read-only (mandatory baseline). Issues: Read and write (optional; only if the App seeds the first issue). Create the App, then note the numeric App ID. Generate a private key; a .pem file downloads. Keep it secret. Install the App on the Community-Access organization, scoped to the template repository and the student repositories (or all repositories if simpler for now). Open the installation and note the numeric Installation ID (it is in the installation settings URL). Verification: you now hold three values: App ID, Installation ID, and the PEM key. Phase 3: Configure variables and secrets Set these on this repository (Settings, Secrets and variables, Actions). Using an Environment named provisioning is recommended so you can add required reviewers. Repository or environment variables: Variable Value PROVISIONING_MODE github-app LEARNING_ROOM_TEMPLATE_REPO Community-Access/learning-room-template PROVISIONING_STUDENT_OWNER Community-Access ADMIN_ROSTER_REPO Community-Access/glow-admin (your admin repo) Secrets: Secret Value PROVISIONING_APP_ID The App ID from Phase 2 PROVISIONING_APP_INSTALLATION_ID The Installation ID from Phase 2 PROVISIONING_APP_PRIVATE_KEY The full contents of the .pem file PRIVATE_STUDENT_DATA_TOKEN A token that can check out and push to the admin repo Using gh for the App" - }, { "id": "admin/HYBRID_REVIEW_INDEX.html", "title": "Hybrid Plan: Review Index", @@ -2129,12 +2123,6 @@ "url": "admin/LEARNING-ROOM-E2E-QA-RUNBOOK.html", "body": "Learning Room End-to-End QA Runbook (Registration to Student Completion) Use this runbook when you want one operational checklist that covers the full workflow: Registration intake and validation. GitHub Classroom deployment and assignment setup. Test account acceptance and repository seeding. Full student walkthrough of every challenge path. Release sign-off evidence for go-live. This runbook intentionally excludes podcast validation work. Everything else is in scope: registration flow, classroom deployment, assignment configuration, template readiness, student progression, PR validation, content validation, skills progression, autograder behavior, challenge completion tracking, and chapter-by-chapter curriculum review. Scope and Audience This runbook is for facilitators, QA leads, and admins who need to verify the complete workshop flow from administrator setup to student completion. Scope Boundaries In scope: Registration workflow behavior and classroom invitation handoff. Classroom assignment creation and autograding configuration. Learning Room automation workflows and facilitator scripts. Full curriculum walkthrough (chapters and appendices). Student challenge journey (1-16) and bonus challenge tracking (A-E). QA evidence capture, defect logging, and release sign-off. Out of scope: Podcast generation, podcast validation, and RSS audio feed checks. Canonical Source Files Used by This Runbook Table: Source files consolidated by this runbook Area Source file Registration entry page REGISTER.md Registration automation admin REGISTRATION-ADMIN.md Registration automation quickstart REGISTRATION-QUICKSTART.md Registration workflow logic .github/workflows/registration.yml Classroom deployment classroom/README.md Assignment copy and autograding setup admin/classroom/README.md Human challenge walkthrough classroom/HUMAN_TEST_MATRIX.md Challenge definitions docs/CHALLENGES.md Student starting path docs/get-going.md Grading criteria classroom/grading-guide.md Release gate baseline GO-LIVE-QA-GUIDE.md Support hub operations SUPPORT_HUB_OPERATIONS.md Table: QA validation checkpoints for registration and classroom automation Table: Student journey checkpoints and expected artifacts Table: Label color and purpose for registration automation Table: Screen reader options for workshop setup Table: Accessibility improvements for screen reader users Required Accounts, Access, and Tools Complete this section before Phase 1. Facilitator admin account ( accesswatch ) with Owner access to both Community-Access and Community-Access-Classroom . Dedicated non-admin test student account for acceptance and full challenge walkthrough. This must be a separate GitHub account that is not an owner or member of either organization. Access to classroom.github.com while signed in as accesswatch . Access to repository settings for Community-Access/git-going-with-github (secrets and variables). Local clone of this repository with PowerShell available. GitHub CLI ( gh ) installed and authenticated as accesswatch for optional verification commands. Critical Precondition Gates (No-Go if any fail) Complete all items below before any cohort launch actions. Facilitator account accesswatch can access both organizations: Community-Access (the workshop and code repository organization) Community-Access-Classroom (the GitHub Classroom organization where student repos are created) accesswatch has a verified email address on its GitHub account and can create and edit Classroom assignments at classroom.github.com . Dedicated non-admin test student account exists and can accept invites. gh auth status succeeds for accesswatch in local terminal. Template repository exists and is set as template repo: Community-Access/learning-room-template Template repository Actions settings allow required automation behavior: Actions enabled GITHUB_TOKEN default workflow permissions include write where required Allow GitHub Actions to create and approve pull requests enabled Registration automation settings are correct when using registration-to-classroom handoff: Variables CLASSROOM_DAY1_ASSIGNMENT_URL and CLASSROOM_DAY2_ASSIGNMENT_URL are set in Community-Access/git-going-with-github Registration entry configuration exists and is valid: Issue form template workshop-registration.yml exists Required labels exist: registration , duplicate , waitlist While signed in as accesswatch , opening classroom.github.com shows the Community-Access-Classroom classroom organization. If any precondition fails, stop and resolve before proceeding. Exact Setup Steps for Keys, Permissions, Settings, and Template Currency Use this section when you need literal setup steps (not only validation checks). A. Confirm facilitator account and organization access You are performing all steps below as accesswatch . If you are currently signed in to GitHub as a different account, sign out first and sign in as accesswatch before continuing. Go to github.com and confirm the top-right avatar shows acce" }, - { - "id": "admin/OWNED_PROVISIONING.html", - "title": "Owned, GitHub-native Provisioning", - "url": "admin/OWNED_PROVISIONING.html", - "body": "Owned, GitHub-native Provisioning This is the operator guide for the GitHub-native provisioning subsystem: the owned replacement for GitHub Classroom described in golden.md and specified in SPEC.md sections 6 and 7. It explains the data model, how to run provisioning, how to recover from failure, and how the optional Flask companion fits in at the edges. For first-time setup and deployment, follow the Hybrid Deployment Guide first. To review the whole deliverable from one place, see HYBRID_REVIEW_INDEX.md . The whole point of this subsystem is captured in one promise: a vendor sunset is a non-event. Provisioning, roster, and progress are owned and reconstructable, so no future change to GitHub Classroom can strand a cohort. Table of contents Why this exists The three owned sources of truth How provisioning works One-time setup Running a cohort Idempotency and self-healing Failure modes and recovery The optional Flask companion Security and least privilege Local development and testing Authoritative Sources Why this exists GitHub Classroom does only two things for this workshop: it copies a template repository into a per-student private repository, and it maps a GitHub identity to a roster entry. Everything else already lives in infrastructure the project controls. This subsystem replaces those two things with code we own, so the critical learner path no longer depends on a single vendor feature. The three owned sources of truth The decoupling contract requires three owned, reconstructable records. See SPEC.md section 6 for the full schemas. Roster of record. One canonical JSON document mapping each learner handle to cohort, path, provisioning state, and progression status. Lives in the private admin repository as roster.json . Schema and validation live in roster.js and roster.schema.json . An example is in examples/roster.example.json . Progress of record. Never authored by a vendor. Derived from deterministic signals the project controls (challenge issue state, PR closing keywords, labels, and the plain-text signals ack and day1-complete ) by progress.js . Provisioning of record. An append-only log of provisioning attempts and outcomes ( provisioning-log.json ), sufficient to prove a repository is correctly seeded and to safely re-run. Reconstruction rule: running the idempotent provisioning action against the roster reproduces a healthy state for every learner, with no third party involved. How provisioning works The provisioning subsystem is plain Node with no third-party dependencies (it uses built-in crypto and global fetch ). The pieces are: File Role roster.js Owned roster: parse, validate, upsert, serialize, redact progress.js Derive learner status from deterministic signals github-app-auth.js Sign an App JWT and mint a short-lived installation token github-client.js Minimal GitHub REST client (fetch or Octokit) provision-core.js The idempotent, serial, backoff provisioning algorithm provision-cli.js Standalone runner used by the workflow provision-learning-rooms.yml Scheduled and manual workflow wrapper The algorithm (SPEC.md section 7.2b) runs serially with a short delay and exponential backoff, not a parallel fan-out, to stay clear of GitHub secondary rate limits. For each pending or failed learner it: checks whether the repository exists; creates it from the template if not; ensures the learner is a collaborator; verifies the required workflow set is present; and records the outcome. Every step is safe to repeat. One-time setup Provisioning supports two modes via the PROVISIONING_MODE variable. Production: GitHub App ( github-app ) A GitHub App is the production identity because it is not tied to a human account, mints short-lived tokens, and uses fine-grained least-privilege permissions. Create a GitHub App in the Community-Access organization with only these permissions: Repository administration (write), Contents (write), Metadata (read), and optionally Issues (write). Nothing more. Install the App on the organization, scoped to the template and student repositories. Generate a private key (PEM). Store these as repository or environment secrets: PROVISIONING_APP_ID PROVISIONING_APP_PRIVATE_KEY (the PEM contents) PROVISIONING_APP_INSTALLATION_ID Set repository variables: PROVISIONING_MODE = github-app LEARNING_ROOM_TEMPLATE_REPO = Community-Access/learning-room-template PROVISIONING_STUDENT_OWNER = Community-Access ADMIN_ROSTER_REPO = the private admin repository holding roster.json Provide PRIVATE_STUDENT_DATA_TOKEN , a token that can check out and push to the admin roster repository. Phase 1 spike only: Actions bot ( actions-bot ) For an early validation spike you may use a least-privilege fine-grained PAT in PROVISIONING_TOKEN with PROVISIONING_MODE = actions-bot . Because the downstream system cannot tell which mode created a repository, spiking with a PAT and graduating to the App loses nothing. Do not use the PAT path for a real cohort: a PAT is bound to a person and is a single point o" - }, { "id": "admin/PODCASTS.html", "title": "Podcasts", @@ -2195,6 +2183,18 @@ "url": "admin/classroom admin access.html", "body": "https://classroom.github.com/classrooms/263509777-git-going-with-github Authoritative Sources Use these official references when you need the current source of truth for facts in this chapter. GitHub Docs, home GitHub Changelog Section-Level Source Map Use this map to verify facts for each major section in this file. File Overview: GitHub Docs, home , GitHub Changelog" }, + { + "id": "admin/OWNED_PROVISIONING.html", + "title": "Owned, GitHub-native Provisioning", + "url": "admin/OWNED_PROVISIONING.html", + "body": "Owned, GitHub-native Provisioning This is the operator guide for the GitHub-native provisioning subsystem: the owned replacement for GitHub Classroom described in golden.md and specified in SPEC.md sections 6 and 7. It explains the data model, how to run provisioning, how to recover from failure, and how the optional Flask companion fits in at the edges. For first-time setup and deployment, follow the Hybrid Deployment Guide first. To review the whole deliverable from one place, see HYBRID_REVIEW_INDEX.md . The whole point of this subsystem is captured in one promise: a vendor sunset is a non-event. Provisioning, roster, and progress are owned and reconstructable, so no future change to GitHub Classroom can strand a cohort. Table of contents Why this exists The three owned sources of truth How provisioning works One-time setup Running a cohort Idempotency and self-healing Failure modes and recovery The optional Flask companion Security and least privilege Local development and testing Authoritative Sources Why this exists GitHub Classroom does only two things for this workshop: it copies a template repository into a per-student private repository, and it maps a GitHub identity to a roster entry. Everything else already lives in infrastructure the project controls. This subsystem replaces those two things with code we own, so the critical learner path no longer depends on a single vendor feature. The three owned sources of truth The decoupling contract requires three owned, reconstructable records. See SPEC.md section 6 for the full schemas. Roster of record. One canonical JSON document mapping each learner handle to cohort, path, provisioning state, and progression status. Lives in the private admin repository as roster.json . Schema and validation live in roster.js and roster.schema.json . An example is in examples/roster.example.json . Progress of record. Never authored by a vendor. Derived from deterministic signals the project controls (challenge issue state, PR closing keywords, labels, and the plain-text signals ack and day1-complete ) by progress.js . Provisioning of record. An append-only log of provisioning attempts and outcomes ( provisioning-log.json ), sufficient to prove a repository is correctly seeded and to safely re-run. Reconstruction rule: running the idempotent provisioning action against the roster reproduces a healthy state for every learner, with no third party involved. How provisioning works The provisioning subsystem is plain Node with no third-party dependencies (it uses built-in crypto and global fetch ). The pieces are: File Role roster.js Owned roster: parse, validate, upsert, serialize, redact progress.js Derive learner status from deterministic signals github-app-auth.js Sign an App JWT and mint a short-lived installation token github-client.js Minimal GitHub REST client (fetch or Octokit) provision-core.js The idempotent, serial, backoff provisioning algorithm provision-cli.js Standalone runner used by the workflow provision-learning-rooms.yml Scheduled and manual workflow wrapper The algorithm (SPEC.md section 7.2b) runs serially with a short delay and exponential backoff, not a parallel fan-out, to stay clear of GitHub secondary rate limits. For each pending or failed learner it: checks whether the repository exists; creates it from the template if not; ensures the learner is a collaborator; verifies the required workflow set is present; and records the outcome. Every step is safe to repeat. One-time setup Provisioning supports two modes via the PROVISIONING_MODE variable. Production: GitHub App ( github-app ) A GitHub App is the production identity because it is not tied to a human account, mints short-lived tokens, and uses fine-grained least-privilege permissions. Create a GitHub App in the Community-Access organization with only these permissions: Repository administration (write), Contents (write), Metadata (read), and optionally Issues (write). Nothing more. Install the App on the organization. Creating student repositories requires an organization-wide installation (repository creation cannot be granted through a selected-repositories installation). Do not skip this step: an App that exists but is not installed fails every token mint with HTTP 404. Generate a private key (PEM). Store these as repository or environment secrets: PROVISIONING_APP_ID PROVISIONING_APP_PRIVATE_KEY (the PEM contents) PROVISIONING_APP_INSTALLATION_ID (optional; when unset, provisioning discovers the installation from the App itself, which also survives re-installation) Set repository variables: PROVISIONING_MODE = github-app LEARNING_ROOM_TEMPLATE_REPO = Community-Access/learning-room-template PROVISIONING_STUDENT_OWNER = Community-Access ADMIN_ROSTER_REPO = the private admin repository holding roster.json PROVISIONING_COHORT_ID = the cohort that new enrollees are added to Provide PRIVATE_STUDENT_DATA_TOKEN , a token that can check out and push to the admin roster repository. Verify credent" + }, + { + "id": "admin/HYBRID_DEPLOYMENT_GUIDE.html", + "title": "Hybrid Deployment Guide", + "url": "admin/HYBRID_DEPLOYMENT_GUIDE.html", + "body": "Hybrid Deployment Guide This is the step-by-step guide to deploying the Hybrid architecture from golden.md and SPEC.md : the owned, GitHub-native provisioning core that replaces GitHub Classroom, plus the optional accessible Flask companion. For the conceptual model and day-to-day operations, see OWNED_PROVISIONING.md . For the legacy Classroom-based flow, see ../classroom/README.md . To review every piece of the Hybrid deliverable from one place, start at HYBRID_REVIEW_INDEX.md . Follow the phases in order. Each phase ends with a verification step. Do not advance until the current phase verifies, exactly as the phased roadmap in golden.md requires. Table of contents What you are deploying Prerequisites Phase 1: Prepare the admin roster repository Phase 2: Create the provisioning GitHub App Phase 3: Configure variables and secrets Phase 4: Deploy and smoke-test provisioning Phase 5: Deploy the optional companion Phase 6: Go-live checklist Operating a cohort Rollback and fallback Reference: configuration surface Authoritative Sources What you are deploying Piece Where it runs Required? Provisioning scripts This repository, .github/scripts/provisioning/ Yes Provisioning workflow GitHub Actions in this repository Yes Roster of record ( roster.json ) A private admin repository Yes Provisioning identity A GitHub App in the Community-Access org Yes (production) Learning Room template Community-Access/learning-room-template Yes (already exists) Flask companion A small host you control (or none) No, optional The critical path is the first five rows. The companion is a convenience at the edges and can be skipped entirely; the issue-form front door and admin-issue dashboard carry the workshop without it. Prerequisites Owner or admin access to the Community-Access GitHub organization. The Learning Room template repository exists and passes template validation ( scripts/classroom/Test-LearningRoomTemplate.ps1 ). A private admin repository you control, to hold the roster and provisioning log. Node.js 20 or newer for local runs and tests. Python 3.12 or newer if you deploy the companion. The gh CLI authenticated, for the convenience commands below. Confirm your toolchain: node --version # v20+ python --version # 3.12+ gh auth status Phase 1: Prepare the admin roster repository The roster of record lives in a private repository so intake data is never public. Create (or choose) a private repository, for example Community-Access/glow-admin . Add a starter roster at its root. Copy the shape from examples/roster.example.json , or start empty: { "version" : 1 , "cohorts" : [ ] , "learners" : [ ] } Commit it as roster.json . The provisioning run will also create and maintain provisioning-log.json next to it. Verification: the file validates locally. node -e "require('./.github/scripts/provisioning/roster').parseRoster(require('fs').readFileSync('roster.json','utf8')); console.log('roster ok')" Phase 2: Create the provisioning GitHub App A GitHub App is the production identity because it is not tied to a human account, mints short-lived tokens, and uses fine-grained least-privilege permissions (SPEC.md section 7.2). Create it once. In the organization, go to Settings, Developer settings, GitHub Apps, New GitHub App. Name it (for example GLOW Provisioning ). Set a homepage URL (the repo is fine). Disable the webhook (this App is called by Actions, not by webhooks). Grant only these repository permissions, nothing more: Administration: Read and write (create student repositories). Contents: Read and write (seed and heal repository content). Metadata: Read-only (mandatory baseline). Issues: Read and write (optional; only if the App seeds the first issue). Create the App, then note the numeric App ID. Generate a private key; a .pem file downloads. Keep it secret. Install the App on the Community-Access organization with All repositories access. Creating new student repositories requires an organization-wide installation; a selected-repositories installation cannot create repos. This step is mandatory: an App that exists but is not installed fails every token mint with HTTP 404, and nothing downstream can work. Open the installation and note the numeric Installation ID (it is in the installation settings URL). Storing it is optional - provisioning discovers it automatically when the secret is unset. Verification: you now hold three values: App ID, Installation ID (optional), and the PEM key. Confirm they work before continuing: run the Provisioning Credentials Health Check workflow, or locally node .github/scripts/provisioning/provision-cli.js --check-auth . Do not proceed to Phase 4 until the check passes. Phase 3: Configure variables and secrets Set these on this repository (Settings, Secrets and variables, Actions). Using an Environment named provisioning is recommended so you can add required reviewers. Repository or environment variables: " + }, { "id": "classroom/assignment-issue-template.html", "title": "Assignment Issue Template Reference", @@ -2339,12 +2339,6 @@ "url": "SHIP-CHECKLIST.html", "body": "Stage 3 Cutover - Ship Checklist Generated by the Stage 3 pre-flight on the current main branch. Everything below has been dry-run verified . The actual cutover is one explicit go away. Pre-cutover state (verified) Stage 2 gate: 5/5 PASS ( scripts/run-stage2-gate.ps1 ). Map: docs/EPISODE_MAP.json validates against schema 1.0.0; 79 episodes (58 ep + 16 cc + 5 cc-bonus); UUIDs stable; track_number drives ordering. Audio inventory: 79 mp3 files present in podcasts/audio/kokoro-am_liam-af_jessica/ ; 58 need rename, 21 already in new scheme. Working tree dirty: 522 uncommitted files from prior Stage 1-2 work. Recommend committing or stashing these before Stage 3. Built tools (this session) Step Tool Status 3.1 podcasts/tools/dry_run_rename.py Dry-run PASS 3.2 podcasts/tools/rename_audio.py (--apply, --finalize-map, --chapters-dir) Preview PASS, preflight clean 3.5 scripts/rewrite-cross-links.ps1 (-Apply) Dry-run PASS, 8 files / 509 reps Dry-run summary Stage 3 . 1 - rename plan 58 mp3 renames (e.g. ch- 00 -welcome.mp3 -> ch- 00 -welcome.mp3) 21 no-op (current_filename == filename) CSV : tmp-proposed-rename.csv Stage 3 . 5 - cross-link rewrite (with .history, snapshots, transcripts excluded) 8 files, 509 replacements Largest : admin\\PODCASTS.md ( 174 ), quality_triage_report.csv/.md ( 224 total), listening -order.json ( 58 ), validate-report.json ( 44 ) Report : tmp-crosslink-rewrite-report.txt Fire sequence (locally reversible up through step 5) Run these commands in order. Each step is a separate commit so any single step can be reverted in isolation. 0. Clean slate cd c:\\code\\git -going-with-github git checkout -b cutover -20260518 git add -A git commit -m "Pre-cutover state: Stage 1-2 work product + Stage 3 tooling" 1. Rename audio + parallel chapter JSON (Stage 3.2) python podcasts\\tools\\rename_audio.py --apply --finalize-map git add -A git commit -m "Stage 3.2: rename audio to topic-prefixed filenames + finalize map" Effect: 58 mp3 files renamed, 58 chapter JSON files renamed, map's current_filename / current_slug updated to match filename . 2. Retag ID3 (Stage 3.3) python podcasts\\tools\\tag_id3.py python podcasts\\tools\\verify_id3.py git add -A git commit -m "Stage 3.3: retag ID3 frames on renamed audio" Effect: TIT2, TRCK, TXXX:TOPIC updated; CHAP/CTOC untouched. Idempotent. 3. Cross-link rewrite (Stage 3.5) powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\re write-cross -links .ps1 -Repo c:\\code\\git -going-with-github -Apply git add -A git commit -m "Stage 3.5: rewrite cross-links to new filename scheme" Effect: 509 replacements across 8 files (listening-order.json, PODCASTS.md, quality_triage_report.*, validate-report.json, etc.). 4. Re-run Stage 2 gate powershell -NoProfile -ExecutionPolicy Bypass -File scripts\\run -stage2-gate .ps1 Must return 5/5 PASS. If not, halt and investigate. 5. Mirror to deploy repo (Stage 3.6) robocopy podcasts\\audio\\kokoro -am_liam-af_jessica c:\\code\\ggg\\site\\media *.mp3 /MIR Confirm file counts match: (Get-ChildItem c:\\code\\ggg\\site\\media\\*.mp3).Count . 6. Regenerate feed + site (Stages 3.7 - 3.8) cd c:\\code\\ggg node generator\\generate -site .js node generator\\validate -feed .js Spot-check c:\\code\\ggg\\site\\feed.xml . Confirm <enclosure url> points to the new filenames. (Cover art piece intentionally omitted per project constraint.) --- IRREVERSIBLE LINE --- Steps below affect remote systems. Confirm step 6 looks correct before proceeding. 7. Deploy (Stage 3.9) c:\\code\\ggg\\ deploy-ggg .ps1 -Backup c:\\code\\ggg\\ deploy-ggg .ps1 -DryRun # review the rsync delta c:\\code\\ggg\\ deploy-ggg .ps1 # live 8. Smoke-test remote curl -I https://lp.csedesigns.com/ggg/feed.xml -> 200 Three sample enclosure URLs -> 200 Three episode pages -> 200 9. Tag (Stage 3.10) cd c:\\code\\git -going-with-github git checkout main git merge --no-ff cutover -20260518 git tag v2 -renumbered-20260518 git push origin main --tags cd c:\\code\\ggg git tag v2 -renumbered-20260518 git push origin main --tags Rollback (steps 1-5) Any of steps 1-5 can be reverted with: git reset --hard HEAD~ 1 For audio rename specifically, tmp-rename-audit.log records every old -> new pair so a reverse rename script can be reconstructed. What's intentionally excluded Cover art ( <itunes:image> in feed) - skipped per project constraint. .history/ files - VS Code Local History snapshots, not rewritten. podcasts/_snapshot-pre-*/ - intentional historical reference. podcasts/transcripts/ - workflow artifacts that key off old slug names in their own filename scheme; do not need rewrite for cutover. Notes All 522 currently-uncommitted files come from prior Stage 1-2 sessions. They have been validated by the Stage 2.8 gate. Committing them in step 0 is the cleanest restore point. The cutover branch should be cutover-20260518 per the REORG-PLAN.md specification. Authoritative Sources Use these official references when you need the current source of truth for t" }, - { - "id": "SPEC.html", - "title": "SPEC.md", - "url": "SPEC.html", - "body": "SPEC.md Complete project specification for GIT Going with GitHub (GLOW). This is the technical source of truth: what the system is, what every component does, how data and state flow, the interfaces between parts, and the requirements every piece must meet. It implements the vision in golden.md . Where this spec and reality disagree, fix one of them and note it here. Table of contents 1. Overview 2. Goals and non-goals 3. Personas and primary journeys 4. System architecture 5. Component catalog 6. Data model and state of record 7. Provisioning subsystem (the Classroom replacement) 8. Automation contracts 9. Curriculum subsystem 10. Content pipeline: docs, HTML, EPUB, audio 11. Optional Flask companion 12. Accessibility requirements 13. Security requirements 14. Reliability, observability, and recovery 15. Testing and quality strategy 16. Configuration surface 17. Repository layout 18. Migration plan: from Classroom to owned provisioning 19. Acceptance criteria 20. Open questions 1. Overview GLOW is a two-day, accessibility-first workshop that teaches blind and low vision technologists to navigate and contribute to open source on GitHub using a screen reader and keyboard alone. It is delivered as: A curriculum of 22 chapters plus appendices in docs/ , published to HTML, EPUB, and an audio podcast series. A Learning Room : a per-student private repository, created from a template, that drives 16 core challenges plus 5 bonus challenges through GitHub-native automation (a PR validation bot named Gandalf, a Student Progression Bot, and a suite of autograders). A registration and cohort system that intakes learners, provisions their Learning Room, releases Day 2 content, and gives facilitators a live status dashboard. The current production system depends on GitHub Classroom for provisioning. This spec defines both the present system and the target system in which Classroom is replaced by owned, GitHub-native provisioning, per golden.md . 2. Goals and non-goals Goals A learner completes the full arc from first GitHub navigation to a real, review-ready open source contribution. Every learner-facing surface is fully operable with NVDA, JAWS, and VoiceOver, by keyboard alone. Provisioning, roster, and progress are owned and reconstructable, with no single vendor as a point of failure. Facilitators can open a cohort, run it, and recover from any failure using documented runbooks. The system degrades gracefully: every automated step has a manual fallback. Non-goals Email delivery is not a dependency. All flows complete using the GitHub web notification inbox. The system does not require learners to join an organization, hold a paid plan, or change Actions settings. The system does not aim to replace VS Code, Copilot, or GitHub itself; it teaches their accessible use. Real-time chat, grading-for-credit, and LMS integration are out of scope. 3. Personas and primary journeys Personas Learner. New-to-GitHub, uses assistive technology, may not code. Needs belonging, clarity, and a forgiving path. Facilitator. Runs a cohort, seeds challenges, monitors progress, recovers stuck learners. Maintainer. Owns the curriculum, automation, content pipeline, and this spec. Primary learner journey Register through an accessible front door (Flask companion, GitHub Pages form, or issue form fallback). Receive a provisioned private Learning Room repository. Acknowledge readiness ( ack ), complete Day 1 challenges, signal day1-complete . Receive Day 2 release, complete Day 2 challenges and the capstone. Open or prepare a real upstream contribution; continue asynchronously with support hub access. Primary facilitator journey Sync and validate the Learning Room template. Open a cohort; provisioning creates student repositories. Seed Challenge 1 (and Challenge 10 for Day 2) per student. Monitor the dashboard; intervene on watchdog alerts. Run teardown after the cohort. 4. System architecture The architecture separates a dependable GitHub-native core (the critical learner path) from an optional companion at the edges. The companion can vanish without breaking any learner. ACCESSIBLE FRONT DOOR (Flask companion OR GitHub Pages form OR issue form fallback) | v REGISTRATION + ROSTER (owned) private admin repo roster record <----+ companion mirror (optional) | v PROVISIONING SUBSYSTEM (GitHub-native) idempotent action: template ----> per-student private Learning Room repo | v THE LEARNING ROOM (per student) Gandalf (PR bot) | Student Progression Bot | Autograders | Challenge issues | v PROGRESSION + DAY 2 RELEASE (deterministic text signals) | v FACILITATOR DASHBOARD (admin issues + optional companion view) Architectural rules: The critical path (front door fallback, provisioning, Learning Room, progression, release) runs entirely on GitHub. The companion only ever renders owned state more nicely; it never holds state the learner depends on. Each arrow is a documented contract (Section 8) with a manual fallback. 5. Component catalog This tab" - }, { "id": "golden.html", "title": "golden.md", @@ -2356,5 +2350,29 @@ "title": "Work.md", "url": "work.html", "body": "Work.md Consolidated walkthrough generated from the challenge issue templates in learning-room/.github/ISSUE_TEMPLATE/ . Do not edit this file manually. Run npm run build:html (or node scripts/generate-work-md.js ) to regenerate. Core challenges Challenge 1: Find Your Way Around Source template: challenge-01-find-your-way.yml Chapters: Ch02: Understanding GitHub | Ch03: Navigating Repositories | Ch04: The Learning Room What you will do: Explore this learning-room repository like a scavenger hunt. You will navigate tabs, the file tree, read key files, and find landmarks. Step-by-step scavenger hunt Task 1: Count files in the root Make sure you are on the Code tab (click it at the top) Look at the file/folder list Count how many items are showing (stop before the first folder) Write down this number Task 2: Find an open issue Click the Issues tab Look for issues with a green Open label Click on one to read it Write down its title Task 3: Read welcome.md Click the Code tab Look for a folder named docs/ Click to open it Find welcome.md and click it Read the first paragraph (the opening sentence) Write down what it says Task 4: Find the repo description Go to the Code tab Look at the top-right area - you should see a text description under the repo name This is the "About" section Write down what it says Task 5: Read the README On the Code tab, scroll down to find README.md Click it to open Find the section that says "Who this workshop is for" Write down the answer Task 6: Check the About section On the Code tab, look at the right sidebar You should see a section labeled "About" with repo information Write down what you see there Your evidence (fill in when closing this issue): 1. I found ___ files in the root of the repository. 2. The open issue I found was titled "___". 3. The first paragraph of welcome.md says... 4. The repository description is... 5. The README says this workshop is for... 6. The About section shows... Peer simulation check After you submit your evidence, open the Peer Simulation: Welcome Link Needs Context issue in this repository and leave an encouraging comment or reaction. If your facilitator gave you access to a real buddy repository, you may use your buddy's issue instead. If you get stuck Symptom Try this I cannot find the tabs Look for a horizontal navigation bar below the repository name. Screen reader users: navigate by heading level 2 or use the landmark navigation. I cannot find docs/welcome.md On the Code tab, look for a folder called docs . Select it, then select welcome.md . I am not sure what counts as evidence Any description of what you found works. There are no wrong answers in a scavenger hunt. I finished but want to check my work View the reference solution Challenge 2: File Your First Issue Source template: challenge-02-first-issue.yml Chapter: Ch05: Working with Issues What you will do: Find a TODO comment in docs/welcome.md , then file an issue describing the problem with a clear title and description. Instructions Open docs/welcome.md and look for a line that contains TODO -- this marks something that needs fixing. Go to the Issues tab and select New issue . Write a clear, descriptive title (not just "Fix TODO"). In the description, use this required format: - What: what needs to change - Where: where the problem is (must include docs/welcome.md ) - Why: why the change matters - Mention the exact TODO text you found Required format (checked automatically) Use this structure in your issue description: What: Replace the placeholder TODO with real welcome text. Where: docs/welcome.md, intro section. Why: New contributors need clear context when they open the file. TODO found: TODO: Add a short workshop welcome paragraph. ```text #### What makes a good issue title? | Instead of this | Write this | |---|---| | Fix TODO | Fix missing workshop description in welcome.md | | Bug | welcome.md TODO placeholder needs real content | | Update file | Replace TODO in welcome.md intro section with actual welcome text | --- **Your evidence** (fill in when closing this issue): ```text Issue URL: https://github.com/... I found a TODO in docs/welcome.md that said... My issue title was... Peer simulation check Open the Peer Simulation: Welcome Link Needs Context issue and leave a comment: Is the title clear? Would you know what needs fixing just from reading the title? If your facilitator gave you access to a real buddy repository, you may review your buddy's issue instead. If you get stuck Symptom Try this I cannot find the TODO Open docs/welcome.md and use your browser's find feature (Ctrl+F or Cmd+F) to search for "TODO". I am not sure what to write in the description Describe the problem as if you are explaining it to someone who has never seen the file. The New issue button is not visible Make sure you are on the Issues tab. If issues are disabled, ask your facilitator. I finished but want to check my work View t" + }, + { + "id": "CLAUDE.html", + "title": "CLAUDE.md", + "url": "CLAUDE.html", + "body": "CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. What This Repository Is "GIT Going with GitHub" - an accessible, screen-reader-first workshop teaching blind and low-vision users to contribute to open source on GitHub. This is primarily a documentation project (Markdown in docs/ ) with three supporting systems: an HTML build pipeline, a podcast audio pipeline, and GitHub Actions automation that provisions and grades per-student "Learning Room" repositories. Commands npm run build:html # Regenerate html/ from Markdown (also regenerates work.md) npm run watch:html # Auto-rebuild HTML on Markdown changes npm run clean # Delete html/ npm run test :automation # node --test .github/scripts/__tests__/*.test.js npm run test :provisioning # node --test .github/scripts/provisioning/__tests__/*.test.js npm run test :llm-execution # Podcast LLM job execution tests # Single test file: node -- test .github/scripts/provisioning/__tests__/roster.test.js npm run validate:podcasts # Podcast catalog + listening-order validation npm run validate:authoritative-sources # Checks "Authoritative Sources" sections in docs Tests use the built-in Node test runner (Node >= 20), no test framework dependency. The podcast audio/TTS steps ( build:podcast-audio , metadata, inventory) require Python and ffmpeg and run locally; treat them as out of scope unless asked. Critical Workflow: HTML Is Committed, Not CI-Built There is no CI build for the docs. After editing any .md file, run npm run build:html and commit both the Markdown source and the regenerated html/ output in the same commit. docs/*.md -> html/docs/*.html , README.md -> html/index.html , plus learning-room/ and root-level docs. The build template and CSS live in scripts/build-html.js . Architecture docs/ - The curriculum: chapters 00 - 22 plus appendix-*.md , CHALLENGES.md (challenge hub), and solutions/ (reference solutions). Note: appendix display letters (A-Z in README tables) do NOT match appendix filenames - they were renumbered; go by the README tables, not the filename letter. learning-room/ - Template repository copied into each student's private repo during provisioning. Its own .github/ holds the student-facing automation (challenge issue templates, validation bot, progression workflows). scripts/generate-work-md.js derives work.md from these challenge templates. .github/scripts/provisioning/ - "Hybrid provisioning": the GitHub-native replacement for GitHub Classroom (which was removed entirely). provision-core.js is pure orchestration - it takes an injected client ( github-client.js ), never touches the network or secrets directly, and returns an updated roster plus a log. Tests run it against a fake client. github-app-auth.js handles GitHub App authentication; roster.js and roster.schema.json define the student roster; provision-cli.js is the entry point; the workflow is provision-learning-rooms.yml . The algorithm (serial, backoff, idempotent) is specified in SPEC.md section 7.2b - keep code and spec in sync. .github/scripts/ (top level) - PR validation and challenge-progression bots. Some are legacy from the old shared-repository model; see .github/README.md for which workflows are active vs. legacy. podcasts/ - Audio companion pipeline: build-bundles.js -> per-episode source bundles -> scripts -> local ONNX TTS (Kokoro default, Piper fallback) -> generate-site.js builds the player page and RSS feed. config/listening-order.json is the canonical episode order shared by JS ( lib/listening-plan.js ) and Python ( listening_plan.py ) resolvers. Audio files are gitignored and hosted on GitHub Releases. scripts/ - Repo-level build tooling (HTML, ePub, BRF, diagram SVGs, source validation). Root-level tmp-* files, work.md / work.html , and golden.md / golden.html are generated or scratch artifacts - do not hand-edit them. Conventions Never use emojis anywhere: docs, code, comments, commit messages (from .github/copilot-instructions.md ). All documentation is written screen-reader-first: semantic headings, descriptive link text, keyboard-only instructions, no meaning conveyed by visuals alone. Docs end with an "Authoritative Sources" section plus a "Section-Level Source Map"; npm run validate:authoritative-sources checks these. Preserve them when editing docs. Commit messages in this repo follow conventional-commit style ( docs: , build: , refactor: , ...). License is CC BY 4.0." + }, + { + "id": "DEPLOYMENT_ASSESSMENT.html", + "title": "Deployment Assessment: Hybrid Provisioning System", + "url": "DEPLOYMENT_ASSESSMENT.html", + "body": "Deployment Assessment: Hybrid Provisioning System Date: June 2, 2026 Status: READY FOR PRODUCTION (with caveat below) Summary The Hybrid provisioning system is architecturally sound, hardened, and production-ready . However, the end-to-end workflow has not yet been tested with real student enrollment . Recommendation: Run Phase 4 smoke test before admitting first cohort. Hardening Assessment by Component OK: GitHub App Configuration Status: HARDENED Why: Fine-grained permissions (least privilege), secrets stored in GitHub Actions only, PEM key never in code Risk: None identified Action: READY OK: Secrets & Variables Management Status: HARDENED Why: All three secrets (App ID, Installation ID, PEM) stored securely in GitHub repository Actions secrets; variables configured correctly Risk: None identified Action: READY OK: Infrastructure Code (Provisioning Scripts) Status: HARDENED Why: Idempotent: Re-running is safe (already-exists vs. created state) Self-healing: Missing students auto-provisioned on next run Deterministic: Uses GitHub API signals, not external state Auditable: All history in provisioning-log.json and git Risk: None identified Action: READY OK: Documentation Updates Status: COMPLETE What: 15+ files updated, 37+ files deleted, HTML regenerated on both sites Coverage: Student-facing, facilitator, operator all covered Risk: None identified Action: READY OK: Public Site Deployments Status: COMPLETE Coverage: community-access.org/git-going-with-github (auto via GitHub Pages) lp.csedesigns.com/ggg (manual via rsync + Caddy reload) Verification: Both sites live and tested (404s work, navigation intact) Risk: None identified Action: READY Warning: End-to-End Student Workflow (NOT YET TESTED) Status: UNTESTED What's missing: Admin roster repo creation Smoke test with test student account Verification that provision workflow runs end-to-end Verification that invitation email sent to test account Verification that student can accept invite and access repo Risk: Medium-logical gaps possible, though architecture is solid Action: REQUIRED-Run Phase 4 before admitting real students What Has Been Tested OK: Automation tests: 78/78 passing (was 98, reduced after removing Classroom tests) OK: Provisioning tests: 55/55 passing OK: HTML regeneration: 393 markdown files -> HTML OK: Site deployments: Both production sites live OK: GitHub App creation: Properly configured with correct permissions OK: Secrets storage: All three values stored securely What Has NOT Been Tested Missing: End-to-end provisioning flow with a real test student Enrollment form submission Issue creation in test account Automation comment with learning room link Provisioning workflow execution Private repo creation and invitation Student repo readiness for challenges Failsafe Mechanisms Mechanism How It Works When It Helps Idempotence Re-running workflow is safe; state tracked in roster.json If automation fails, re-run fixes it Self-healing Missing students auto-provisioned on next run If student missed by error, they're caught on retry Audit Trail provisioning-log.json records every action Troubleshooting, rollback, verification Source of Truth roster.json is canonical; all state derives from it Repo can be recreated, roster is immutable Least Privilege GitHub App has minimal permissions Limits blast radius if credentials compromised Secrets in Actions PEM never stored in code or logs Prevents accidental exposure Deterministic Signals Progress tracked via GitHub events No hidden external dependencies Recommendations Before Go-Live MUST DO (Blocking) Run Phase 4 Smoke Test Create test admin roster repo Add one test learner (e.g., a throwaway GitHub account you control) Trigger provisioning workflow with dry-run first Trigger provisioning workflow without dry-run Verify: Private repo created Invitation sent to test account Roster entry updated to "provisioned" provisioning-log.json records "created" Run again; verify log records "already-exists" Test account accepts invite; repo is ready for challenges Verify End-to-End Student Flow Submit enrollment form as test student Verify issue created in your account Reply ack to issue Verify automation comment posts learning room link Verify you receive GitHub invitation Accept invitation; verify repo has challenges SHOULD DO (Recommended, not blocking) Verify Flask Companion (Optional) If deploying companion for web enrollment, test locally first See Phase 5 of deployment guide Document Rollback Plan If provisioning fails, how to revert? If student repo corrupted, how to re-provision? Document in runbook Test with Multiple Students Once Phase 4 passes, add 3-5 test students to roster Verify all get provisioned correctly Verify no duplicates or conflicts NICE TO HAVE (After launch) Monitor first week of production: Track provisioning success rate Log any failures to support channel Verify no duplicate repos Verify all students receive invitations Set up alert" + }, + { + "id": "SPEC.html", + "title": "SPEC.md", + "url": "SPEC.html", + "body": "SPEC.md Complete project specification for GIT Going with GitHub (GLOW). This is the technical source of truth: what the system is, what every component does, how data and state flow, the interfaces between parts, and the requirements every piece must meet. It implements the vision in golden.md . Where this spec and reality disagree, fix one of them and note it here. Table of contents 1. Overview 2. Goals and non-goals 3. Personas and primary journeys 4. System architecture 5. Component catalog 6. Data model and state of record 7. Provisioning subsystem (the Classroom replacement) 8. Automation contracts 9. Curriculum subsystem 10. Content pipeline: docs, HTML, EPUB, audio 11. Optional Flask companion 12. Accessibility requirements 13. Security requirements 14. Reliability, observability, and recovery 15. Testing and quality strategy 16. Configuration surface 17. Repository layout 18. Migration plan: from Classroom to owned provisioning 19. Acceptance criteria 20. Open questions 1. Overview GLOW is a two-day, accessibility-first workshop that teaches blind and low vision technologists to navigate and contribute to open source on GitHub using a screen reader and keyboard alone. It is delivered as: A curriculum of 22 chapters plus appendices in docs/ , published to HTML, EPUB, and an audio podcast series. A Learning Room : a per-student private repository, created from a template, that drives 16 core challenges plus 5 bonus challenges through GitHub-native automation (a PR validation bot named Gandalf, a Student Progression Bot, and a suite of autograders). A registration and cohort system that intakes learners, provisions their Learning Room, releases Day 2 content, and gives facilitators a live status dashboard. The current production system depends on GitHub Classroom for provisioning. This spec defines both the present system and the target system in which Classroom is replaced by owned, GitHub-native provisioning, per golden.md . 2. Goals and non-goals Goals A learner completes the full arc from first GitHub navigation to a real, review-ready open source contribution. Every learner-facing surface is fully operable with NVDA, JAWS, and VoiceOver, by keyboard alone. Provisioning, roster, and progress are owned and reconstructable, with no single vendor as a point of failure. Facilitators can open a cohort, run it, and recover from any failure using documented runbooks. The system degrades gracefully: every automated step has a manual fallback. Non-goals Email delivery is not a dependency. All flows complete using the GitHub web notification inbox. The system does not require learners to join an organization, hold a paid plan, or change Actions settings. The system does not aim to replace VS Code, Copilot, or GitHub itself; it teaches their accessible use. Real-time chat, grading-for-credit, and LMS integration are out of scope. 3. Personas and primary journeys Personas Learner. New-to-GitHub, uses assistive technology, may not code. Needs belonging, clarity, and a forgiving path. Facilitator. Runs a cohort, seeds challenges, monitors progress, recovers stuck learners. Maintainer. Owns the curriculum, automation, content pipeline, and this spec. Primary learner journey Register through an accessible front door (Flask companion, GitHub Pages form, or issue form fallback). Receive a provisioned private Learning Room repository. Acknowledge readiness ( ack ), complete Day 1 challenges, signal day1-complete . Receive Day 2 release, complete Day 2 challenges and the capstone. Open or prepare a real upstream contribution; continue asynchronously with support hub access. Primary facilitator journey Sync and validate the Learning Room template. Open a cohort; provisioning creates student repositories. Seed Challenge 1 (and Challenge 10 for Day 2) per student. Monitor the dashboard; intervene on watchdog alerts. Run teardown after the cohort. 4. System architecture The architecture separates a dependable GitHub-native core (the critical learner path) from an optional companion at the edges. The companion can vanish without breaking any learner. ACCESSIBLE FRONT DOOR (Flask companion OR GitHub Pages form OR issue form fallback) | v REGISTRATION + ROSTER (owned) private admin repo roster record <----+ companion mirror (optional) | v PROVISIONING SUBSYSTEM (GitHub-native) idempotent action: template ----> per-student private Learning Room repo | v THE LEARNING ROOM (per student) Gandalf (PR bot) | Student Progression Bot | Autograders | Challenge issues | v PROGRESSION + DAY 2 RELEASE (deterministic text signals) | v FACILITATOR DASHBOARD (admin issues + optional companion view) Architectural rules: The critical path (front door fallback, provisioning, Learning Room, progression, release) runs entirely on GitHub. The companion only ever renders owned state more nicely; it never holds state the learner depends on. Each arrow is a documented contract (Section 8) with a manual fallback. 5. Component catalog This tab" + }, + { + "id": "REVIEW-2026-07-02.html", + "title": "Operations Review: Provisioning Failures and Workshop Model Robustness", + "url": "REVIEW-2026-07-02.html", + "body": "Operations Review: Provisioning Failures and Workshop Model Robustness Date: July 2, 2026 Scope: GitHub Actions failure analysis, provisioning subsystem, registration pipeline, repo hygiene. Implementation status (July 2, 2026): everything in sections 3, 4.1-4.4, and 6 that is code has been implemented (installation-ID auto-discovery, watchdog alert issue, work-free early exit, event-driven provisioning dispatch, weekly credentials health check, preflight assertions, intake-to-roster sync, Classroom link removal, [REGISTER] private intake plus redaction, authoritative-sources gate fixes, scratch cleanup). The remaining manual step is section 2: install the GitHub App on the Community-Access organization. Template drift (4.5) is still open. 1. Headline Finding: The Provisioning GitHub App Is Not Installed on the Org The "Provision Learning Rooms" workflow has failed on every real run since it was deployed. Verified numbers from the GitHub API: 280 total runs of provision-learning-rooms.yml . Exactly 1 success ever (June 2, 2026, 20:16 UTC) - and that run was a dry run, which skips authentication entirely. Every non-dry run, including roughly 12 scheduled runs per day for the past 30 days, fails with: Failed to mint installation token (HTTP 404) Root cause, confirmed directly: GET /orgs/Community-Access/installations shows exactly one GitHub App installed on the org - digitalocean (app id 64711). The provisioning App was created (App ID and PEM are stored as secrets) but was never installed on the Community-Access organization. There is no installation to mint a token against, so the token mint returns 404 no matter what PROVISIONING_APP_INSTALLATION_ID contains. Consequences, also confirmed: The private roster ( git-going-with-github-administration/roster.json ) has 2 learners in provision_state: pending - unchanged since June 2. Two registered people have been waiting a month for learning rooms that were never created. The admin repo has had no roster commits since June 2 because the workflow dies before the commit step. This is precisely the failure class SPEC.md's golden principle warns about ("never let a single point of failure hold a cohort hostage") and violates the section 7.2b invariant "any failure visible to a human before a learner notices." The watchdog the spec calls for does not exist. Note on the local working-tree changes: the uncommitted edits to github-app-auth.js (normalize URL-form installation IDs, better 404 hint) are good hardening and all 58 provisioning tests pass, but they will not fix production. No installation ID is valid until the App is installed. 2. Immediate Fix (Operator Steps, ~15 Minutes) Install the App: GitHub -> Organization settings -> Developer settings -> GitHub Apps -> your provisioning App -> Install App -> Community-Access. Because the App must create new repositories, install it org-wide (repository creation cannot be granted through a selected-repositories installation). Capture the installation ID from the post-install URL ( .../settings/installations/<id> ) or from GET /app/installations using an App JWT. Update the PROVISIONING_APP_INSTALLATION_ID secret (it lives in the provisioning environment, not repo-level secrets). Run the workflow manually with dry_run: true (expect: "2 learner(s) would be provisioned"), then run it for real. Verify: two new learning-room-* repos, invitations sent, roster entries flipped to provisioned , provisioning-log.json committed to the admin repo. 3. Make the Failure Class Impossible: Code Changes These are ordered by payoff. 3.1 Auto-discover the installation ID Stop storing the installation ID as a secret at all. With an App JWT you can call GET /app/installations and select the installation whose account matches PROVISIONING_STUDENT_OWNER . Add this as the fallback (or the default) in github-app-auth.js / provision-cli.js . This removes an entire class of copy-paste misconfiguration and survives App re-installation (installation IDs change when an App is uninstalled and reinstalled). Alternative for the workflow path: the official actions/create-github-app-token action needs only app-id and private-key and resolves the installation from the repo owner automatically. It keeps the zero-npm-dependency property of the repo since it runs as an action, and the CLI keeps the hand-rolled path for local use. 3.2 Alert on failure - implement the spec's watchdog Thirty days of silent failure proves the need. Add a final if: failure() step to provision-learning-rooms.yml that creates or updates a pinned, labeled issue (for example provisioning-broken ) in this repo, including the error line from the log. Close it automatically on the next success. Requires issues: write on this workflow only. 3.3 Skip work-free runs Most of the 280 failed runs had zero learners to provision. Read the roster first; if there are no pending / failed entries, exit success before minting" } ] diff --git a/scripts/validate-authoritative-sources.js b/scripts/validate-authoritative-sources.js index 91231c63..f8cb934c 100644 --- a/scripts/validate-authoritative-sources.js +++ b/scripts/validate-authoritative-sources.js @@ -39,6 +39,10 @@ function includeContentFile(relPath) { if (excludedPrefixes.some(prefix => n.startsWith(prefix))) return false; + // Tooling instructions, not curriculum content. + const excludedRootFiles = ['CLAUDE.md']; + if (excludedRootFiles.includes(n)) return false; + if (n.startsWith('docs/')) return true; if (n.startsWith('classroom/')) return true; if (n.startsWith('learning-room/')) return true; diff --git a/tmp-audio-inventory.csv b/tmp-audio-inventory.csv deleted file mode 100644 index c11bd1e7..00000000 --- a/tmp-audio-inventory.csv +++ /dev/null @@ -1,80 +0,0 @@ -filename,slug,group,size_bytes,duration_seconds,sha256 -cc-01-find-your-way-around.mp3,cc-01-find-your-way-around,challenge,5192781,551.76,2ac5d556b0e3e0409e03bf0882943c79815c21bfade31548c0fe60aabe478242 -cc-02-file-your-first-issue.mp3,cc-02-file-your-first-issue,challenge,5564229,590.88,fe55d0f5a4c0bc2fae61529a186d4906cb119c40a38ed4ac231e1a421baaabdb -cc-03-join-the-conversation.mp3,cc-03-join-the-conversation,challenge,5684973,610.82,4ede8541e3746347fa0feb9892c9645455505900f14c9b9abfaa101b9c70564a -cc-04-branch-out.mp3,cc-04-branch-out,challenge,5506389,583.8,e9a49a6179cb2daa255717438c71d4f23e2c3e3710aaca4d1834193706b72391 -cc-05-make-your-mark.mp3,cc-05-make-your-mark,challenge,5705205,609.34,876271134fb914e2dc01a5781bbc1ae4066aa91d2d5c356693dbc078fee5d9c2 -cc-06-open-your-first-pr.mp3,cc-06-open-your-first-pr,challenge,6085077,646.39,b3fe24598785d3492714b89b4e246218a58506ba502d041075fdc5daf00980af -cc-07-survive-a-merge-conflict.mp3,cc-07-survive-a-merge-conflict,challenge,5427693,575.57,f0ea227c7eb423ab7d0d1e6cf77cf92fc631dd9b9badfed128144159a31d36fd -cc-08-open-source-culture.mp3,cc-08-open-source-culture,challenge,6975933,738.14,cbe5805a83143dcb25b527cd6a8c5f468ef441d3eb45a37af882432cfce6e58c -cc-09-merge-day.mp3,cc-09-merge-day,challenge,5969901,633.46,3ea80a84ff92fe77472aa4967a157f93cca1ef9047c71d725291e633dd9f3252 -cc-10-go-local.mp3,cc-10-go-local,challenge,6547245,696.17,9150698d45b0c64b2dce3cdf98bb4fd96a0668951798578eaf6a8bc8ec47f543 -cc-11-day-2-pull-request.mp3,cc-11-day-2-pull-request,challenge,4403925,469.63,d084df5e44bbb5de4a659ccb40c93a4a06fc579e4ae2fe53902da4608291bba5 -cc-12-code-review.mp3,cc-12-code-review,challenge,5401677,569.3,df81fb998e2ea08552458d216e9de23eeacbecf99fbe32d1942e8a6a5e711b25 -cc-13-copilot-as-collaborator.mp3,cc-13-copilot-as-collaborator,challenge,7149741,753.65,394fd8b2e2765779fb3ee136cd105994d0700cb36b064e1e801c5f3f408f9546 -cc-14-design-an-issue-template.mp3,cc-14-design-an-issue-template,challenge,6050973,639.6,1312bc190d949d5c013b568ddf3b9cedd9d660ce3211ce237897edd5503e0051 -cc-15-discover-accessibility-agents.mp3,cc-15-discover-accessibility-agents,challenge,9357189,986.45,c37887db12a5fa73bbb5758902cfd00a710d60832931653d58ef65a7c7a88f16 -cc-16-build-your-own-agent.mp3,cc-16-build-your-own-agent,challenge,6740325,711.1,03b7c13c5807c4184e0e91f96441fab415cd4390c42c291b80d64a8c39cf6f61 -cc-bonus-a-improve-agent.mp3,cc-bonus-a-improve-agent,bonus,5491557,582.02,780c478d6f6ad369e4930cdbd2fb0d35665f9cf977bec7840fd9eb3593b4ba16 -cc-bonus-b-document-your-journey.mp3,cc-bonus-b-document-your-journey,bonus,6242133,662.23,7d83d8be22446d32a702a15aed54e5def17639e7ee3517e9467aa92a305f9ce4 -cc-bonus-c-group-challenge.mp3,cc-bonus-c-group-challenge,bonus,7668981,815.18,1123525fd698f31b854331a1688b334f43e3c63d9effdcdbaad53bfda37f90a5 -cc-bonus-d-notifications.mp3,cc-bonus-d-notifications,bonus,5068389,532.42,95e2e2bedd1aeadcb82f6a6f7c072a9105b259ea813e5a1f2e980d65f7938f53 -cc-bonus-e-git-history.mp3,cc-bonus-e-git-history,bonus,6182589,656.21,99320384351c97628e963c6d56e1a16368bb3dfd14593a5c59a24189f1b40dfa -ep00-welcome.mp3,ep00-welcome,ep,4990485,527.57,ef4a56491493cd0e6f282f787436e5bf5c323bbe2e12b62641d7c90cf4fc894b -ep01-pre-workshop-setup.mp3,ep01-pre-workshop-setup,ep,7507795,781.58,67b81be12cc630920059687625e26080fd8957ee37d5bc3bce574bf1a4d9fcbd -ep02-github-web-structure.mp3,ep02-github-web-structure,ep,5060829,539.59,142b9d9f942bb1f13b6c5237cc37e7feab2da781f1e4c3cfe619b4758911d02d -ep03-navigating-repositories.mp3,ep03-navigating-repositories,ep,5322381,564.38,8174c26623dc17b4dfaaa3f8d16579a2eecea41d62bdf98561a00752570615bf -ep04-the-learning-room.mp3,ep04-the-learning-room,ep,6211821,660.14,b27f8f85fab9252dbf0c6fd4d9aad9ca309b9e8519b6e0bb23a5fdb1f1a13181 -ep05-working-with-issues.mp3,ep05-working-with-issues,ep,8757933,927.36,26435fd2001d42c1d78427368ebc0c4f7888a5ee9cee7d3a28c02355e856c5ae -ep06-working-with-pull-requests.mp3,ep06-working-with-pull-requests,ep,7336677,772.18,20e1ca4581ac2c380a8eb324e147f69a73f52d4cfe0c1a246dbf2ee8f53af1b1 -ep07-merge-conflicts.mp3,ep07-merge-conflicts,ep,5942205,634.49,faf914f4faed8a385d91ffc6293080ec3899ecfd02442003366f710ae8251e18 -ep08-culture-and-etiquette.mp3,ep08-culture-and-etiquette,ep,6423765,681.34,70aad15f30bf94461ff9e5144fcd30ac2662aa90b406ddfcf962977bde8c98be -ep09-labels-milestones-projects.mp3,ep09-labels-milestones-projects,ep,7175229,757.06,410572857dfd0cb68590ab36e6004841319c81c82750f78e15b041b600e4c84a -ep10-notifications.mp3,ep10-notifications,ep,5460693,574.94,bbd5193a17d4cbf4bf667bcae135f0880583195c184528a7a4073a5958a89ed9 -ep11-vscode-basics.mp3,ep11-vscode-basics,ep,7639941,802.51,8f67f5bbc322c65325aee509484e1ed875ce8117bd0df63c104df6ab40dcff42 -ep12-git-source-control.mp3,ep12-git-source-control,ep,7503957,793.73,718634acfa2ec57284fde5ffe3b9717c55548f28866a03d401a4be0fdb028b75 -ep13-github-prs-extension.mp3,ep13-github-prs-extension,ep,6720813,710.38,d2a649255ad76e7a318959ba4a7f5af3854f94043c4345c4c6efe1a81339b683 -ep14-github-copilot.mp3,ep14-github-copilot,ep,8977869,939.89,68cb171343a81d2214af2785cee4ab31d19cdee3b1eada681fc4fbc4b2f08a09 -ep15-accessible-code-review.mp3,ep15-accessible-code-review,ep,6820989,720.38,b080c9eab803d7d7f4efdc15d04f74e75ef86be704904485c0902ce3581cc292 -ep16-issue-templates.mp3,ep16-issue-templates,ep,7923597,839.93,6ceb1a140a38a86788a21e1e76ed998f0f1c3a538019e9adf96a30897d9bd4af -ep17-accessibility-agents.mp3,ep17-accessibility-agents,ep,7846653,831.02,e5b6df770324e632aefc817c2c9767699f945ed8bd84434d12dc25df2dbbf3f7 -ep18-glossary.mp3,ep18-glossary,ep,8530389,909.02,2778567aa083afc54581e3383f900473394b96d5b59c1328634033a2fb5c15da -ep19-screen-reader-cheatsheet.mp3,ep19-screen-reader-cheatsheet,ep,6772677,709.99,41af8927d498559c4e46b4161f56df8fe3eeafc4aafd20cc6726cabc3ad6b051 -ep20-accessibility-standards.mp3,ep20-accessibility-standards,ep,5894085,621.38,4e93c0adea412b6204d92a164549c1aba77afcf5909ee6913c5bb7bd2a27dfe8 -ep21-git-authentication.mp3,ep21-git-authentication,ep,6313557,664.82,76d44ee5f1340e34ff682209ac329c6bc36216170ef21ae2a6f4207cc3f2039f -ep22-github-flavored-markdown.mp3,ep22-github-flavored-markdown,ep,7722669,814.1,8a9f92e8e8359ab685c47382ff1ad67f7488e3cdfdf5e14c04cd0f4ae12840ac -ep23-github-gists.mp3,ep23-github-gists,ep,5665557,600.6,95651ed47e1692e7e781f34f2c8cadc97e374a0a8f85b180718828505a8a833b -ep24-github-discussions.mp3,ep24-github-discussions,ep,6289317,670.37,2bf1f6f3ac270ee4feaa489545a246ecbe3f0e794561f343ac614c8175781e43 -ep25-releases-tags-insights.mp3,ep25-releases-tags-insights,ep,7356909,780.96,3d4ec8aca47c771ac929cf514ef61daf520ded7391b71f90a985ffb9d6692d2c -ep26-github-projects.mp3,ep26-github-projects,ep,5316981,559.73,c174184342e3e565f841f5cebcbfd63c353986ce5d21e8b908dc64189e4ed045 -ep27-advanced-search.mp3,ep27-advanced-search,ep,6683061,705.43,0f052c222f66691bc8d250ef1db8f047c486526a6f68bb189670a825db7370cb -ep28-branch-protection.mp3,ep28-branch-protection,ep,6304485,662.64,ab4961445a36b4a9e05ed65a722495b5d73883fee0527e1f7807271875d3b812 -ep29-security-features.mp3,ep29-security-features,ep,6389997,675.12,813b0d765501a02679f3c7e28ef5ba5cba4d7d6763584399bc4e651f4bc72863 -ep30-vscode-accessibility-reference.mp3,ep30-vscode-accessibility-reference,ep,8737149,912.26,929712aafc0758565668e2c999a07fb745befffd49998ca7f9964a48333c7097 -ep31-github-codespaces.mp3,ep31-github-codespaces,ep,5681829,597.43,bbdaeb9c1d09a1754785ff2657ad998d28a836bba3d0a6ad5929bb91d1ee3c6c -ep32-github-mobile.mp3,ep32-github-mobile,ep,5582757,586.46,38ecdf8e3f0a663d602fae5850b911d97a39f0e6151f8a47a152342f76a41a60 -ep33-github-pages.mp3,ep33-github-pages,ep,5937741,626.54,a94a154e0907ae82a31f0c4e623a99927b201f57a195fc2ac05da701cb9234af -ep34-github-actions.mp3,ep34-github-actions,ep,5804949,613.22,f0415d7e0eb761309ae70b9933e6b9ce7c82f5a30fbb49d634b19d9883ff487c -ep35-profile-sponsors-wikis.mp3,ep35-profile-sponsors-wikis,ep,5330685,567.38,decb0e9e4f790c3cf6a9bc66ef1c928dae71264fcac29c2cc7bdba4d696f54df -ep36-organizations-templates.mp3,ep36-organizations-templates,ep,6503085,692.78,c789a2983d0f75ca343f61e249ef07a74e754297954dd0c7f5db3465e5f75029 -ep37-contributing-to-open-source.mp3,ep37-contributing-to-open-source,ep,8555445,906.96,7e217c44d341de372a8c267b793d91672ed5d4948bd9a9a40bb2ebbe0f2830b0 -ep38-resources.mp3,ep38-resources,ep,7079421,744.22,277b2902036bce88a1cf3926bea82e439d469b87a51d7ad82888d42f54dfb30f -ep39-accessibility-agents-reference.mp3,ep39-accessibility-agents-reference,ep,8280669,868.32,a29f0553a8a76b04ac6665e0de345036628f07c8797e220336839840006f5fb5 -ep40-copilot-reference.mp3,ep40-copilot-reference,ep,7876989,825.91,6fc992d14731a2d7dbc8daa952dd3ce7b0653fb4d231528939788f0c85f0e1b1 -ep41-copilot-models.mp3,ep41-copilot-models,ep,6057477,635.52,5c51bd4b5a4dfe21b5512749431c23f174a240019ea02ad1956efb72d20b0dee -ep42-accessing-workshop-materials.mp3,ep42-accessing-workshop-materials,ep,5380077,567.91,571927cd15e1399133f447d7cedff74cade8b26e92182a9a51c67920444ea0ee -ep43-github-skills-catalog.mp3,ep43-github-skills-catalog,ep,5609469,599.09,9393413252483233ca30b547b53b3e9649c39247e44a7c2937d263847a254804 -ep44-choose-your-tools.mp3,ep44-choose-your-tools,ep,5968245,631.75,d1f570ddaf68bfe502f10f59db3ba0f9cced08750cfd7e2cd50a963606acc03c -ep45-vscode-accessibility-deep-dive.mp3,ep45-vscode-accessibility-deep-dive,ep,6943149,731.78,9ecb7316d0e57837179dbd27ba90be7d3c0ab868d029445c09b02ec7fb3e4442 -ep46-how-git-works.mp3,ep46-how-git-works,ep,5601285,593.04,f8e4037bf6f5125c7853beda0004c14e3dcbdc9da2469ef69cfbedc9e7eb70d0 -ep47-fork-and-contribute.mp3,ep47-fork-and-contribute,ep,7339869,785.9,f97a3aac11c0a0099ff88fe19b20509704723bf955b3b63201c30864fa28486a -ep48-build-your-agent-capstone.mp3,ep48-build-your-agent-capstone,ep,6055701,639.1,5a53da9f7212e09d451e3156a04528b453ea1df091840c218c48fd4b5f5c51d7 -ep49-github-accessibility-and-open-source.mp3,ep49-github-accessibility-and-open-source,ep,3697629,392.33,8291b3e47f712cc610f987dcb1759448c0413e5a96dde438b9c421b400cca802 -ep50-advanced-git-operations.mp3,ep50-advanced-git-operations,ep,6574533,697.56,2dbfaa808939666d6f5eddfb2fcea6fe9351aecdf2bf39f07e3b2ff032946f58 -ep51-git-security-for-contributors.mp3,ep51-git-security-for-contributors,ep,6819645,716.14,3317744fd78510e4c5846850f7af5fdfa05e27bb24944b3d6e01df0ce59e8470 -ep52-github-desktop.mp3,ep52-github-desktop,ep,6306453,665.93,259a0ac88e1238b7f4080034f4cb42ee23966f3c068c611ffde63d886f086745 -ep53-github-cli-reference.mp3,ep53-github-cli-reference,ep,7365741,780.0,072fc9e5e5564c5edbbed4f330b867811cfe2709a41fa1f4b10c7b91fc905f92 -ep54-agent-installation-setup.mp3,ep54-agent-installation-setup,ep,6096597,639.31,7c571dd788e2a4654cba51b838607bc81a55850285758699596365b49d183e59 -ep55-advanced-agent-patterns.mp3,ep55-advanced-agent-patterns,ep,5628597,590.06,272c59af030c34b2b8fa2573b9e57ad2a71e47e1b471818648fd5c6419e2d1ce -ep56-document-developer-tools.mp3,ep56-document-developer-tools,ep,6324357,663.74,86b61173936be77b6b200f618f6519b9d30a180437c7f74ee52b961552279dc7 -ep79-what-comes-next.mp3,ep79-what-comes-next,ep,6147405,649.34,7737b4edee7cc4516368cbce9d6aea27cb2d1c52405de7446de46b4c73f8e593 diff --git a/tmp-crosslink-rewrite-report.txt b/tmp-crosslink-rewrite-report.txt deleted file mode 100644 index 0823e7ed..00000000 --- a/tmp-crosslink-rewrite-report.txt +++ /dev/null @@ -1,17 +0,0 @@ -# Stage 3.5 cross-link rewrite report -# Repo: c:\code\git-going-with-github -# Mode: APPLY - -FILE REORG-PLAN.md (1 replacements) -FILE SHIP-CHECKLIST.md (1 replacements) -FILE admin\PODCASTS.md (174 replacements) -FILE podcasts\validate-report.json (44 replacements) -FILE podcasts\config\listening-order.json (58 replacements) -FILE podcasts\tools\quality_triage_report.csv (112 replacements) -FILE podcasts\tools\quality_triage_report.md (112 replacements) -FILE podcasts\tools\agentic-pilot\README.md (3 replacements) -FILE scripts\rewrite-cross-links.ps1 (5 replacements) - -# Totals -Files changed: 9 -Replacements: 510 diff --git a/tmp-learning-room-smoke-test.ps1 b/tmp-learning-room-smoke-test.ps1 deleted file mode 100644 index 7eca60ce..00000000 --- a/tmp-learning-room-smoke-test.ps1 +++ /dev/null @@ -1,370 +0,0 @@ -param( - [Parameter(Mandatory = $true)] - [string]$Repo, - - [Parameter(Mandatory = $true)] - [string]$Dir, - - [string]$User = 'accesswatch' -) - -$ErrorActionPreference = 'Stop' - -function Get-IssueByPrefix { - param( - [string]$Prefix, - [string]$State = 'all' - ) - - $issues = gh issue list -R $Repo --state $State --limit 200 --json number,title,state,url | ConvertFrom-Json - $match = $issues | Where-Object { $_.title -like "$Prefix*" } | Select-Object -First 1 - if (-not $match) { - throw "Issue not found for prefix: $Prefix" - } - return $match -} - -function Get-LatestRunId { - param( - [string]$Workflow, - [string]$Event = '' - ) - - $args = @('run', 'list', '-R', $Repo, '--workflow', $Workflow, '--limit', '1', '--json', 'databaseId') - if ($Event) { - $args += @('--event', $Event) - } - - $raw = & gh @args - if (-not $raw) { - return '' - } - - $runs = $raw | ConvertFrom-Json - if (-not $runs -or $runs.Count -eq 0) { - return '' - } - - return [string]$runs[0].databaseId -} - -function Wait-NewRun { - param( - [string]$Workflow, - [string]$PreviousId, - [string]$Event = '' - ) - - for ($attempt = 0; $attempt -lt 40; $attempt += 1) { - $current = Get-LatestRunId -Workflow $Workflow -Event $Event - if ($current -and $current -ne $PreviousId) { - return $current - } - } - - throw "No new run detected for workflow $Workflow" -} - -function Watch-TriggeredRun { - param( - [scriptblock]$Action, - [string]$Workflow, - [string]$Event = '' - ) - - $previous = Get-LatestRunId -Workflow $Workflow -Event $Event - & $Action - $runId = Wait-NewRun -Workflow $Workflow -PreviousId $previous -Event $Event - & gh run watch $runId -R $Repo --exit-status | Out-Null - return $runId -} - -function Assert-CommentContains { - param( - [int]$IssueNumber, - [string]$Needle - ) - - $comments = gh api "repos/$Repo/issues/$IssueNumber/comments" | ConvertFrom-Json - $found = $comments | Where-Object { $_.body -like "*$Needle*" } | Select-Object -First 1 - if (-not $found) { - throw "Did not find expected comment text '$Needle' on issue or PR #$IssueNumber" - } -} - -function Sync-Main { - Push-Location $Dir - try { - git checkout main | Out-Null - git fetch origin | Out-Null - git pull --ff-only origin main | Out-Null - } - finally { - Pop-Location - } -} - -function New-TestPr { - param( - [string]$Branch, - [string]$RelativePath, - [string]$Content, - [string]$CommitMessage, - [string]$Title, - [string]$Body, - [string[]]$ExpectedWorkflows, - [switch]$Append - ) - - Sync-Main - Push-Location $Dir - try { - git checkout -b $Branch | Out-Null - - $fullPath = Join-Path $Dir $RelativePath - $parent = Split-Path $fullPath -Parent - if (-not (Test-Path $parent)) { - New-Item -ItemType Directory -Path $parent -Force | Out-Null - } - - if ($Append) { - Add-Content -Path $fullPath -Value $Content - } - else { - Set-Content -Path $fullPath -Value $Content - } - - git add $RelativePath - git commit -m $CommitMessage | Out-Null - git push -u origin $Branch | Out-Null - - $previousRuns = @{} - foreach ($workflow in $ExpectedWorkflows) { - $previousRuns[$workflow] = Get-LatestRunId -Workflow $workflow -Event 'pull_request' - } - - gh pr create -R $Repo --base main --head $Branch --title $Title --body $Body | Out-Null - - $pr = gh pr list -R $Repo --head $Branch --state open --json number,url | ConvertFrom-Json | Select-Object -First 1 - if (-not $pr) { - throw "PR not created for branch $Branch" - } - - foreach ($workflow in $ExpectedWorkflows) { - $runId = Wait-NewRun -Workflow $workflow -PreviousId $previousRuns[$workflow] -Event 'pull_request' - gh run watch $runId -R $Repo --exit-status | Out-Null - } - - return $pr - } - finally { - Pop-Location - } -} - -function Merge-TestPr { - param([int]$PrNumber) - - $previousStudent = Get-LatestRunId -Workflow 'student-progression.yml' -Event 'issues' - $previousSkills = Get-LatestRunId -Workflow 'skills-progression.yml' -Event 'pull_request' - - gh pr merge $PrNumber -R $Repo --merge --delete-branch --admin | Out-Null - - $skillsRun = Wait-NewRun -Workflow 'skills-progression.yml' -PreviousId $previousSkills -Event 'pull_request' - gh run watch $skillsRun -R $Repo --exit-status | Out-Null - - $studentRun = Wait-NewRun -Workflow 'student-progression.yml' -PreviousId $previousStudent -Event 'issues' - gh run watch $studentRun -R $Repo --exit-status | Out-Null -} - -Write-Output "Starting smoke test for $Repo" - -Watch-TriggeredRun -Action { gh workflow run student-progression.yml -R $Repo -f start_challenge=1 -f assignee=$User | Out-Null } -Workflow 'student-progression.yml' -Event 'workflow_dispatch' | Out-Null -$challenge1 = Get-IssueByPrefix -Prefix 'Challenge 1:' -State 'open' -Write-Output "Challenge 1 created: #$($challenge1.number)" - -Watch-TriggeredRun -Action { gh issue close $challenge1.number -R $Repo --comment 'Smoke test: close challenge 1 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge2 = Get-IssueByPrefix -Prefix 'Challenge 2:' -State 'open' -Write-Output "Challenge 2 created: #$($challenge2.number)" - -Watch-TriggeredRun -Action { - gh issue create -R $Repo -t 'Replace TODO in welcome.md intro section' -b "What: Replace the placeholder TODO with real welcome text.`nWhere: docs/welcome.md, intro section.`nWhy: New contributors need clear context when they open the file.`nTODO found: TODO: Add a short workshop welcome paragraph." | Out-Null -} -Workflow 'autograder-issue-filed.yml' -Event 'issues' | Out-Null -$studentIssue = Get-IssueByPrefix -Prefix 'Replace TODO in welcome.md intro section' -State 'open' -Assert-CommentContains -IssueNumber $studentIssue.number -Needle '## Challenge 2: First issue filed!' -Write-Output "Challenge 2 feedback verified on issue #$($studentIssue.number)" - -Watch-TriggeredRun -Action { gh issue close $challenge2.number -R $Repo --comment 'Smoke test: close challenge 2 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge3 = Get-IssueByPrefix -Prefix 'Challenge 3:' -State 'open' -Watch-TriggeredRun -Action { gh issue comment $challenge3.number -R $Repo -b '@bot help' | Out-Null } -Workflow 'pr-validation-bot.yml' -Event 'issue_comment' | Out-Null -Assert-CommentContains -IssueNumber $challenge3.number -Needle 'Here are some helpful resources' -Write-Output 'Comment responder verified on challenge 3' - -Watch-TriggeredRun -Action { gh issue close $challenge3.number -R $Repo --comment 'Smoke test: close challenge 3 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge4 = Get-IssueByPrefix -Prefix 'Challenge 4:' -State 'open' -Watch-TriggeredRun -Action { gh issue close $challenge4.number -R $Repo --comment 'Smoke test: close challenge 4 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge5 = Get-IssueByPrefix -Prefix 'Challenge 5:' -State 'open' -Watch-TriggeredRun -Action { gh issue close $challenge5.number -R $Repo --comment 'Smoke test: close challenge 5 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge6 = Get-IssueByPrefix -Prefix 'Challenge 6:' -State 'open' -Write-Output 'Challenge 6 ready for PR workflow test' - -$pr1Body = "## What this PR does`nAdds a small smoke-test update to welcome guidance.`n`n## Why`nThis exercises the Learning Room Day 1 PR feedback workflows end to end.`n`nCloses #$($challenge6.number)" -$pr1 = New-TestPr -Branch 'learn/accesswatch-smoke' -RelativePath 'docs/welcome.md' -Content "`nSmoke test PR 1: validate Day 1 workflow feedback." -CommitMessage 'docs: add smoke test line to welcome guidance' -Title 'docs: validate Day 1 workflow feedback' -Body $pr1Body -ExpectedWorkflows @( - 'Challenge 5: Branch Commit Check', - 'Challenge 10: Local Commit Check', - 'Challenge 6: PR Linked to Issue Check', - 'Challenge 7: Merge Conflict Resolution Check', - 'PR Validation & Feedback Bot', - 'Accessibility & Content Validation' -) -Append -Assert-CommentContains -IssueNumber $pr1.number -Needle 'Welcome to Your First Pull Request' -Assert-CommentContains -IssueNumber $pr1.number -Needle 'PR Validation Report' -Assert-CommentContains -IssueNumber $pr1.number -Needle 'Content Validation Report' -Assert-CommentContains -IssueNumber $pr1.number -Needle '## Challenge 5:' -Assert-CommentContains -IssueNumber $pr1.number -Needle '## Challenge 6:' -Assert-CommentContains -IssueNumber $pr1.number -Needle '## Challenge 7:' -Write-Output "PR 1 feedback verified on PR #$($pr1.number)" -Merge-TestPr -PrNumber $pr1.number -Assert-CommentContains -IssueNumber $pr1.number -Needle '## Achievement Unlocked' -$challenge7 = Get-IssueByPrefix -Prefix 'Challenge 7:' -State 'open' -Write-Output 'Challenge 7 created after merging PR 1' - -Watch-TriggeredRun -Action { gh issue close $challenge7.number -R $Repo --comment 'Smoke test: close challenge 7 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge8 = Get-IssueByPrefix -Prefix 'Challenge 8:' -State 'open' -Watch-TriggeredRun -Action { gh issue close $challenge8.number -R $Repo --comment 'Smoke test: close challenge 8 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge9 = Get-IssueByPrefix -Prefix 'Challenge 9:' -State 'open' -Watch-TriggeredRun -Action { gh issue close $challenge9.number -R $Repo --comment 'Smoke test: close challenge 9 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge10 = Get-IssueByPrefix -Prefix 'Challenge 10:' -State 'open' -Watch-TriggeredRun -Action { gh issue close $challenge10.number -R $Repo --comment 'Smoke test: close challenge 10 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge11 = Get-IssueByPrefix -Prefix 'Challenge 11:' -State 'open' -Write-Output 'Challenge 11 ready for Day 2 PR workflow test' - -$pr2Body = "## What this PR does`nAdds a small smoke-test README update for local-workflow validation.`n`n## Why`nThis exercises the Day 2 PR and local commit feedback path.`n`nCloses #$($challenge11.number)" -$pr2 = New-TestPr -Branch 'fix/accesswatch-smoke' -RelativePath 'README.md' -Content "`nSmoke test PR 2: validate Day 2 workflow feedback." -CommitMessage 'docs: add smoke test line to README' -Title 'docs: validate Day 2 workflow feedback' -Body $pr2Body -ExpectedWorkflows @( - 'Challenge 5: Branch Commit Check', - 'Challenge 10: Local Commit Check', - 'Challenge 6: PR Linked to Issue Check', - 'PR Validation & Feedback Bot', - 'Accessibility & Content Validation' -) -Append -Assert-CommentContains -IssueNumber $pr2.number -Needle 'PR Validation Report' -Assert-CommentContains -IssueNumber $pr2.number -Needle 'Content Validation Report' -Assert-CommentContains -IssueNumber $pr2.number -Needle '## Challenge 10:' -Assert-CommentContains -IssueNumber $pr2.number -Needle '## Challenge 6:' -Write-Output "PR 2 feedback verified on PR #$($pr2.number)" -Merge-TestPr -PrNumber $pr2.number -Assert-CommentContains -IssueNumber $pr2.number -Needle '## Achievement Unlocked' -$challenge12 = Get-IssueByPrefix -Prefix 'Challenge 12:' -State 'open' - -Watch-TriggeredRun -Action { gh issue close $challenge12.number -R $Repo --comment 'Smoke test: close challenge 12 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge13 = Get-IssueByPrefix -Prefix 'Challenge 13:' -State 'open' -Watch-TriggeredRun -Action { gh issue close $challenge13.number -R $Repo --comment 'Smoke test: close challenge 13 to validate progression.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge14 = Get-IssueByPrefix -Prefix 'Challenge 14:' -State 'open' -Write-Output 'Challenge 14 ready for template PR workflow test' - -$templateContent = @" -name: ""Smoke Test Template"" -description: ""Template created by automated smoke test"" -title: ""[SMOKE]: "" -labels: [""custom-template""] -body: - - type: markdown - attributes: - value: | - ## Smoke Test Template - This template exists to validate the workflow path. - - type: textarea - id: details - attributes: - label: ""Details"" - description: ""Describe your request."" - validations: - required: true -"@ -$pr3Body = "## What this PR does`nAdds a custom issue template for smoke testing.`n`n## Why`nThis validates the Challenge 14 template workflow end to end.`n`nCloses #$($challenge14.number)" -$pr3 = New-TestPr -Branch 'template-remix/accesswatch-smoke' -RelativePath '.github/ISSUE_TEMPLATE/accesswatch-smoke-template.yml' -Content $templateContent -CommitMessage 'feat: add smoke test issue template' -Title 'feat: validate Challenge 14 template workflow' -Body $pr3Body -ExpectedWorkflows @( - 'Challenge 5: Branch Commit Check', - 'Challenge 10: Local Commit Check', - 'Challenge 6: PR Linked to Issue Check', - 'Challenge 14: Issue Template Validation', - 'PR Validation & Feedback Bot', - 'Accessibility & Content Validation' -) -Assert-CommentContains -IssueNumber $pr3.number -Needle 'PR Validation Report' -Assert-CommentContains -IssueNumber $pr3.number -Needle 'Content Validation Report' -Assert-CommentContains -IssueNumber $pr3.number -Needle '## Challenge 14:' -Write-Output "PR 3 feedback verified on PR #$($pr3.number)" -Merge-TestPr -PrNumber $pr3.number -Assert-CommentContains -IssueNumber $pr3.number -Needle '## Achievement Unlocked' -$challenge15 = Get-IssueByPrefix -Prefix 'Challenge 15:' -State 'open' - -Watch-TriggeredRun -Action { gh issue close $challenge15.number -R $Repo --comment 'Smoke test: close challenge 15 to validate progression and bonus unlocks.' | Out-Null } -Workflow 'student-progression.yml' -Event 'issues' | Out-Null -$challenge16 = Get-IssueByPrefix -Prefix 'Challenge 16:' -State 'open' -$bonusA = Get-IssueByPrefix -Prefix 'Bonus A:' -State 'open' -$bonusB = Get-IssueByPrefix -Prefix 'Bonus B:' -State 'open' -$bonusC = Get-IssueByPrefix -Prefix 'Bonus C:' -State 'open' -$bonusD = Get-IssueByPrefix -Prefix 'Bonus D:' -State 'open' -$bonusE = Get-IssueByPrefix -Prefix 'Bonus E:' -State 'open' -Write-Output 'Challenge 16 and bonus issues created' - -$agentContent = @" ---- -name: ""Smoke Test Agent"" -description: ""Validates the capstone workflow path"" -tools: [""codebase""] ---- - -# Smoke Test Agent - -## Responsibilities -- Confirm the capstone validation workflow runs. -- Provide a minimal valid agent file for testing. -- Keep the smoke test deterministic. - -## Guardrails -- Never change unrelated files. -- Always keep the content minimal and valid. -"@ -$pr4Body = "## What this PR does`nAdds a minimal valid agent file for smoke testing.`n`n## Why`nThis validates the Challenge 16 capstone workflow end to end.`n`nCloses #$($challenge16.number)" -$pr4 = New-TestPr -Branch 'capstone/accesswatch-smoke' -RelativePath 'agents/smoke-test-agent.md' -Content $agentContent -CommitMessage 'feat: add smoke test agent file' -Title 'feat: validate Challenge 16 capstone workflow' -Body $pr4Body -ExpectedWorkflows @( - 'Challenge 5: Branch Commit Check', - 'Challenge 10: Local Commit Check', - 'Challenge 6: PR Linked to Issue Check', - 'Challenge 16: Capstone Agent File Validation', - 'PR Validation & Feedback Bot', - 'Accessibility & Content Validation' -) -Assert-CommentContains -IssueNumber $pr4.number -Needle 'PR Validation Report' -Assert-CommentContains -IssueNumber $pr4.number -Needle 'Content Validation Report' -Assert-CommentContains -IssueNumber $pr4.number -Needle '## Challenge 16:' -Write-Output "PR 4 feedback verified on PR #$($pr4.number)" -Merge-TestPr -PrNumber $pr4.number -Assert-CommentContains -IssueNumber $pr4.number -Needle '## Achievement Unlocked' - -$failedRuns = gh run list -R $Repo --status failure --limit 100 --json workflowName,displayTitle,url | ConvertFrom-Json -if ($failedRuns -and $failedRuns.Count -gt 0) { - $failedRuns | ForEach-Object { - Write-Output "FAILED: $($_.workflowName) | $($_.displayTitle) | $($_.url)" - } - throw 'Smoke test detected failed workflow runs.' -} - -$allIssues = gh issue list -R $Repo --state all --limit 200 --json number,title,state | ConvertFrom-Json -foreach ($index in 1..16) { - $prefix = "Challenge ${index}:" - $match = $allIssues | Where-Object { $_.title -like "$prefix*" } | Select-Object -First 1 - if (-not $match) { - throw "Missing expected issue for $prefix" - } -} -foreach ($bonusPrefix in @('Bonus A:','Bonus B:','Bonus C:','Bonus D:','Bonus E:')) { - $match = $allIssues | Where-Object { $_.title -like "$bonusPrefix*" } | Select-Object -First 1 - if (-not $match) { - throw "Missing expected issue for $bonusPrefix" - } -} - -Write-Output 'SMOKE TEST PASSED' -Write-Output "Repo: https://github.com/$Repo" -Write-Output "PRs verified: #$($pr1.number), #$($pr2.number), #$($pr3.number), #$($pr4.number)" -Write-Output "Bonuses verified: #$($bonusA.number), #$($bonusB.number), #$($bonusC.number), #$($bonusD.number), #$($bonusE.number)" diff --git a/tmp-learning-room-smoke-wrapper.ps1 b/tmp-learning-room-smoke-wrapper.ps1 deleted file mode 100644 index 758ecfd3..00000000 --- a/tmp-learning-room-smoke-wrapper.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -$ErrorActionPreference = 'Stop' - -$timestamp = Get-Date -Format 'yyyyMMddHHmmss' -$repoName = "learning-room-smoke-$timestamp" -$repo = "accesswatch/$repoName" -$dir = Join-Path 'C:\code' $repoName -$sourceDir = 'C:\code\learning-room-template' -$harness = 'C:\code\git-going-with-github\tmp-learning-room-smoke-test.ps1' - -if (Test-Path $dir) { - throw "Temp dir already exists: $dir" -} - -New-Item -ItemType Directory -Path $dir | Out-Null -robocopy $sourceDir $dir /E /XD .git node_modules > $null - -Push-Location $dir -try { - git init -b main | Out-Null - git config user.name 'accesswatch' - git config user.email 'accesswatch@users.noreply.github.com' - git add . - git commit -m 'Smoke test seed' | Out-Null - gh repo create $repo --private --source . --remote origin --push | Out-Null -} -finally { - Pop-Location -} - -Write-Output "SMOKE_REPO=$repo" -Write-Output "SMOKE_DIR=$dir" - -& $harness -Repo $repo -Dir $dir diff --git a/tmp-proposed-feed-order.csv b/tmp-proposed-feed-order.csv deleted file mode 100644 index a2bbc35b..00000000 --- a/tmp-proposed-feed-order.csv +++ /dev/null @@ -1,80 +0,0 @@ -track_number,filename,topic_prefix,learning_band,title,pairs_with -1,ch-00-welcome.mp3,ch,chapter,Chapter 00: Welcome to Git Going with GitHub, -2,ch-01-pre-workshop-setup.mp3,ch,chapter,Chapter 01: Pre-Workshop Setup, -3,cc-01-find-your-way-around.mp3,cc,challenge,Challenge 01: Find Your Way Around,ch-01-pre-workshop-setup.mp3 -4,ch-02-github-web-structure.mp3,ch,chapter,Chapter 02: Understanding GitHub on the Web, -5,cc-02-file-your-first-issue.mp3,cc,challenge,Challenge 02: File Your First Issue,ch-02-github-web-structure.mp3 -6,ch-03-navigating-repositories.mp3,ch,chapter,Chapter 03: Navigating Repositories, -7,cc-03-join-the-conversation.mp3,cc,challenge,Challenge 03: Join the Conversation,ch-03-navigating-repositories.mp3 -8,ch-04-the-learning-room.mp3,ch,chapter,Chapter 04: The Learning Room, -9,cc-04-branch-out.mp3,cc,challenge,Challenge 04: Branch Out,ch-04-the-learning-room.mp3 -10,ch-05-working-with-issues.mp3,ch,chapter,Chapter 05: Working with Issues, -11,cc-05-make-your-mark.mp3,cc,challenge,Challenge 05: Make Your Mark,ch-05-working-with-issues.mp3 -12,ch-06-working-with-pull-requests.mp3,ch,chapter,Chapter 06: Working with Pull Requests, -13,cc-06-open-your-first-pr.mp3,cc,challenge,Challenge 06: Open Your First Pull Request,ch-06-working-with-pull-requests.mp3 -14,ch-07-merge-conflicts.mp3,ch,chapter,Chapter 07: Merge Conflicts Are Not Scary, -15,cc-07-survive-a-merge-conflict.mp3,cc,challenge,Challenge 07: Survive a Merge Conflict,ch-07-merge-conflicts.mp3 -16,ch-08-culture-and-etiquette.mp3,ch,chapter,Chapter 08: Open Source Culture and Etiquette, -17,cc-08-open-source-culture.mp3,cc,challenge,Challenge 08: The Culture Layer,ch-08-culture-and-etiquette.mp3 -18,ch-09-labels-milestones-projects.mp3,ch,chapter,"Chapter 09: Labels, Milestones, and Projects", -19,cc-09-merge-day.mp3,cc,challenge,Challenge 09: Merge Day,ch-09-labels-milestones-projects.mp3 -20,ch-10-notifications.mp3,ch,chapter,Chapter 10: Notifications and Mentions, -21,cc-10-go-local.mp3,cc,challenge,Challenge 10: Go Local,ch-10-notifications.mp3 -22,ch-11-vscode-basics.mp3,ch,chapter,Chapter 11: VS Code Setup and Accessibility, -23,cc-11-day-2-pull-request.mp3,cc,challenge,Challenge 11: Open a Day 2 PR,ch-11-vscode-basics.mp3 -24,ch-12-git-source-control.mp3,ch,chapter,Chapter 12: Git and Source Control in VS Code, -25,cc-12-code-review.mp3,cc,challenge,Challenge 12: Review Like a Pro,ch-12-git-source-control.mp3 -26,ch-13-github-prs-extension.mp3,ch,chapter,Chapter 13: The GitHub Pull Requests Extension, -27,cc-13-copilot-as-collaborator.mp3,cc,challenge,Challenge 13: AI as Your Copilot,ch-13-github-prs-extension.mp3 -28,ch-14-github-copilot.mp3,ch,chapter,Chapter 14: GitHub Copilot, -29,cc-14-design-an-issue-template.mp3,cc,challenge,Challenge 14: Template Remix,ch-14-github-copilot.mp3 -30,ch-15-accessible-code-review.mp3,ch,chapter,Chapter 15: Accessible Code Review, -31,cc-15-discover-accessibility-agents.mp3,cc,challenge,Challenge 15: Meet the Agents,ch-15-accessible-code-review.mp3 -32,ch-16-issue-templates.mp3,ch,chapter,Chapter 16: Issue Templates, -33,cc-16-build-your-own-agent.mp3,cc,challenge,Challenge 16: Build Your Agent (Capstone),ch-16-issue-templates.mp3 -34,ch-17-accessibility-agents.mp3,ch,chapter,Chapter 17: Accessibility Agents, -35,ch-18-how-git-works.mp3,ch,chapter,Chapter 18: How Git Works: The Mental Model, -36,ch-19-fork-and-contribute.mp3,ch,chapter,Chapter 19: Fork and Contribute, -37,ch-20-build-your-agent-capstone.mp3,ch,chapter,Chapter 20: Build Your Agent: Capstone, -38,ch-21-contributing-to-open-source.mp3,ch,chapter,Chapter 21: Contributing to Open Source, -39,ch-22-what-comes-next.mp3,ch,chapter,Chapter 22: What Comes Next, -40,cc-bonus-a-improve-agent.mp3,cc-bonus,bonus,Bonus A: Improve an Agent, -41,cc-bonus-b-document-your-journey.mp3,cc-bonus,bonus,Bonus B: Document Your Journey, -42,cc-bonus-c-group-challenge.mp3,cc-bonus,bonus,Bonus C: Group Challenge, -43,cc-bonus-d-notifications.mp3,cc-bonus,bonus,Bonus D: Notifications, -44,cc-bonus-e-git-history.mp3,cc-bonus,bonus,Bonus E: Git History, -45,ref-01-glossary.mp3,ref,ref,Reference 01: Glossary of Terms, -46,ref-02-screen-reader-cheatsheet.mp3,ref,ref,Reference 02: Screen Reader Cheat Sheet, -47,ref-03-accessibility-standards.mp3,ref,ref,Reference 03: Accessibility Standards Reference, -48,ref-04-resources.mp3,ref,ref,Reference 04: Resources and Links, -49,ref-05-accessing-workshop-materials.mp3,ref,ref,Reference 05: Accessing Workshop Materials, -50,ref-06-github-skills-catalog.mp3,ref,ref,Reference 06: GitHub Skills - Complete Course Catalog, -51,git-01-git-authentication.mp3,git,git,Git Toolkit 01: Git Authentication, -52,git-02-advanced-git-operations.mp3,git,git,Git Toolkit 02: Advanced Git Operations, -53,git-03-git-security-for-contributors.mp3,git,git,Git Toolkit 03: Git Security for Contributors, -54,git-04-github-desktop.mp3,git,git,Git Toolkit 04: GitHub Desktop, -55,git-05-github-cli-reference.mp3,git,git,Git Toolkit 05: GitHub CLI Reference, -56,tools-01-github-flavored-markdown.mp3,tools,tools,Tooling 01: GitHub Flavored Markdown, -57,tools-02-github-gists.mp3,tools,tools,Tooling 02: GitHub Gists, -58,tools-03-github-discussions.mp3,tools,tools,Tooling 03: GitHub Discussions, -59,tools-04-releases-tags-insights.mp3,tools,tools,"Tooling 04: Releases, Tags, and Insights", -60,tools-05-github-projects.mp3,tools,tools,Tooling 05: GitHub Projects Deep Dive, -61,tools-06-advanced-search.mp3,tools,tools,Tooling 06: Advanced Search, -62,tools-07-github-codespaces.mp3,tools,tools,Tooling 07: GitHub Codespaces, -63,tools-08-github-mobile.mp3,tools,tools,Tooling 08: GitHub Mobile, -64,tools-09-github-pages.mp3,tools,tools,Tooling 09: Publishing with GitHub Pages, -65,tools-10-github-actions.mp3,tools,tools,Tooling 10: GitHub Actions and Workflows, -66,tools-11-profile-sponsors-wikis.mp3,tools,tools,"Tooling 11: Profile, Sponsors, and Wikis", -67,tools-12-organizations-templates.mp3,tools,tools,Tooling 12: Organizations and Templates, -68,agents-01-accessibility-agents-reference.mp3,agents,agents,Agents 01: Accessibility Agents - Complete Reference, -69,agents-02-copilot-reference.mp3,agents,agents,Agents 02: Copilot Reference, -70,agents-03-copilot-models.mp3,agents,agents,Agents 03: Copilot Billing and Models, -71,agents-04-choose-your-tools.mp3,agents,agents,Agents 04: Choose Your Tools, -72,agents-05-agent-installation-setup.mp3,agents,agents,Agents 05: Agent Installation and Setup, -73,agents-06-advanced-agent-patterns.mp3,agents,agents,Agents 06: Advanced Agent Patterns, -74,sec-01-branch-protection.mp3,sec,sec,Security 01: Branch Protection and Rulesets, -75,sec-02-security-features.mp3,sec,sec,Security 02: GitHub Security Features, -76,a11y-01-vscode-accessibility-reference.mp3,a11y,a11y,Accessibility 01: VS Code Accessibility Reference, -77,a11y-02-vscode-accessibility-deep-dive.mp3,a11y,a11y,Accessibility 02: VS Code Accessibility Deep Dive, -78,a11y-03-github-accessibility-and-open-source.mp3,a11y,a11y,Accessibility 03: GitHub Accessibility and Open Source at Scale, -79,a11y-04-document-developer-tools.mp3,a11y,a11y,Accessibility 04: Document Developer Tools, diff --git a/tmp-proposed-rename.csv b/tmp-proposed-rename.csv deleted file mode 100644 index 3664121b..00000000 --- a/tmp-proposed-rename.csv +++ /dev/null @@ -1,59 +0,0 @@ -uuid,track_number,group,current_filename,filename -5782d363-05ad-4015-a53b-69311bf4a024,1,ep,ep00-welcome.mp3,ch-00-welcome.mp3 -e7b269f2-4c6b-4644-bfce-0aaf2f8937fe,2,ep,ep01-pre-workshop-setup.mp3,ch-01-pre-workshop-setup.mp3 -941a3a2b-554f-41dc-96b9-5b98443054b1,4,ep,ep02-github-web-structure.mp3,ch-02-github-web-structure.mp3 -a3fc92a0-46cb-4be3-9a61-74ebc8a0b429,6,ep,ep03-navigating-repositories.mp3,ch-03-navigating-repositories.mp3 -a4c497d9-535e-4040-a3e4-bfeffb9401ae,8,ep,ep04-the-learning-room.mp3,ch-04-the-learning-room.mp3 -e2c04356-2759-42bf-ac4a-3f1326db1213,10,ep,ep05-working-with-issues.mp3,ch-05-working-with-issues.mp3 -6c7d19fd-bd1e-4be6-adb2-9f0b48a552c8,12,ep,ep06-working-with-pull-requests.mp3,ch-06-working-with-pull-requests.mp3 -7c1f250f-100d-40e5-b2de-5f6cb05d6e9f,14,ep,ep07-merge-conflicts.mp3,ch-07-merge-conflicts.mp3 -03c96ea0-7cfa-428c-bd62-c1f6b8f70318,16,ep,ep08-culture-and-etiquette.mp3,ch-08-culture-and-etiquette.mp3 -825f9c85-a86e-4b2c-a4b2-4f33de3767f3,18,ep,ep09-labels-milestones-projects.mp3,ch-09-labels-milestones-projects.mp3 -a9c6f562-4499-4659-8e98-dc33365bb389,20,ep,ep10-notifications.mp3,ch-10-notifications.mp3 -e20fe72e-c09c-4c1c-9c35-a3d6dd03dbb7,22,ep,ep11-vscode-basics.mp3,ch-11-vscode-basics.mp3 -f12549cd-8ed9-40b9-9e82-5f1cc2f02cfe,24,ep,ep12-git-source-control.mp3,ch-12-git-source-control.mp3 -825592b8-b6e3-40b3-a62e-27a3592f92f3,26,ep,ep13-github-prs-extension.mp3,ch-13-github-prs-extension.mp3 -97ff2e66-6125-4ec5-afae-cbbfb4529873,28,ep,ep14-github-copilot.mp3,ch-14-github-copilot.mp3 -47ac569d-8fda-4ab9-848f-195728a37863,30,ep,ep15-accessible-code-review.mp3,ch-15-accessible-code-review.mp3 -d0070b6b-f6b1-4c60-af6c-da79b163374b,32,ep,ep16-issue-templates.mp3,ch-16-issue-templates.mp3 -5d406c5d-fda1-428c-9b18-63a2c37c12bf,34,ep,ep17-accessibility-agents.mp3,ch-17-accessibility-agents.mp3 -ae6eeda0-18af-4915-8c10-1b7ec69b212c,35,ep,ep46-how-git-works.mp3,ch-18-how-git-works.mp3 -3cb0f067-09b5-4328-8613-734b5294e428,36,ep,ep47-fork-and-contribute.mp3,ch-19-fork-and-contribute.mp3 -917558eb-d37b-46e8-af5d-d1f6e3816cc1,37,ep,ep48-build-your-agent-capstone.mp3,ch-20-build-your-agent-capstone.mp3 -34ba0bf7-f732-4eff-a700-610f2d640003,38,ep,ep37-contributing-to-open-source.mp3,ch-21-contributing-to-open-source.mp3 -8e96a939-869f-406c-bf9f-196677343056,39,ep,ep79-what-comes-next.mp3,ch-22-what-comes-next.mp3 -c4cb6bdb-7999-4d4a-87cc-3855192ea413,45,ep,ep18-glossary.mp3,ref-01-glossary.mp3 -a0edc294-39b9-4c4b-8b88-0184fcb61941,46,ep,ep19-screen-reader-cheatsheet.mp3,ref-02-screen-reader-cheatsheet.mp3 -efb32923-7fb1-4c3a-9d8f-c1c5bf2caae1,47,ep,ep20-accessibility-standards.mp3,ref-03-accessibility-standards.mp3 -fdb46e34-3dfe-4109-9374-7c68340163e4,48,ep,ep38-resources.mp3,ref-04-resources.mp3 -f6c09e20-76ed-4107-8317-e785fc400187,49,ep,ep42-accessing-workshop-materials.mp3,ref-05-accessing-workshop-materials.mp3 -56c7b915-f209-4468-9be1-501ffbe76fc8,50,ep,ep43-github-skills-catalog.mp3,ref-06-github-skills-catalog.mp3 -de290bca-ca2a-4303-8cb9-84c0ee7abf9e,51,ep,ep21-git-authentication.mp3,git-01-git-authentication.mp3 -3d60a94f-0060-4077-86d7-7378059ec042,52,ep,ep50-advanced-git-operations.mp3,git-02-advanced-git-operations.mp3 -adecbc6a-1d8d-41e1-ab51-e69722edfa3f,53,ep,ep51-git-security-for-contributors.mp3,git-03-git-security-for-contributors.mp3 -63b03ed9-a69d-49cf-a285-bd34235899b1,54,ep,ep52-github-desktop.mp3,git-04-github-desktop.mp3 -c1bbc918-7686-4959-84e5-b70bc4b75b42,55,ep,ep53-github-cli-reference.mp3,git-05-github-cli-reference.mp3 -ec21e605-c5ca-4ea5-93dc-44d9c3f3dead,56,ep,ep22-github-flavored-markdown.mp3,tools-01-github-flavored-markdown.mp3 -e4850faf-13b1-43b2-9d66-cc82740efc34,57,ep,ep23-github-gists.mp3,tools-02-github-gists.mp3 -af3521b2-0271-4dc0-b725-aed7771fe0ae,58,ep,ep24-github-discussions.mp3,tools-03-github-discussions.mp3 -faa0a825-d870-43df-97e8-b4b921f95aff,59,ep,ep25-releases-tags-insights.mp3,tools-04-releases-tags-insights.mp3 -5cc23d77-6061-4092-8d00-cd10d90b631d,60,ep,ep26-github-projects.mp3,tools-05-github-projects.mp3 -df063762-71bf-42e5-9fe2-c4e45a942ac1,61,ep,ep27-advanced-search.mp3,tools-06-advanced-search.mp3 -1134a33e-60ba-4192-a256-952384b39e1d,62,ep,ep31-github-codespaces.mp3,tools-07-github-codespaces.mp3 -c337d0ac-3e0e-41db-a12e-898919b8e08e,63,ep,ep32-github-mobile.mp3,tools-08-github-mobile.mp3 -3d23be9d-f3a3-479e-9814-e494a11e06f9,64,ep,ep33-github-pages.mp3,tools-09-github-pages.mp3 -83f42282-5dd3-4155-9809-a309af1566f8,65,ep,ep34-github-actions.mp3,tools-10-github-actions.mp3 -afe0af59-e122-4946-b257-83820bb772f6,66,ep,ep35-profile-sponsors-wikis.mp3,tools-11-profile-sponsors-wikis.mp3 -f9b69058-5a1f-4550-8540-68310a5a2bbd,67,ep,ep36-organizations-templates.mp3,tools-12-organizations-templates.mp3 -3ba6d2d5-808a-4f31-a8ac-088f38f5655b,68,ep,ep39-accessibility-agents-reference.mp3,agents-01-accessibility-agents-reference.mp3 -60e9e20d-59dc-4618-b21f-eba8a2f087e7,69,ep,ep40-copilot-reference.mp3,agents-02-copilot-reference.mp3 -bd284e08-3097-4b46-a237-c586cf916d3e,70,ep,ep41-copilot-models.mp3,agents-03-copilot-models.mp3 -9db086ca-c6c0-476d-bfdb-74fb16ed4970,71,ep,ep44-choose-your-tools.mp3,agents-04-choose-your-tools.mp3 -54161b03-64e0-4aec-867b-360ce3846a3a,72,ep,ep54-agent-installation-setup.mp3,agents-05-agent-installation-setup.mp3 -19d7104a-29aa-4ec0-8d16-c03735d76e86,73,ep,ep55-advanced-agent-patterns.mp3,agents-06-advanced-agent-patterns.mp3 -a3f97849-1fa6-4a48-b0b8-232002bcaf0c,74,ep,ep28-branch-protection.mp3,sec-01-branch-protection.mp3 -bd10a748-164c-48a5-a02c-d75be1243a81,75,ep,ep29-security-features.mp3,sec-02-security-features.mp3 -a8f94597-272c-45c9-982c-b06cd9adc54a,76,ep,ep30-vscode-accessibility-reference.mp3,a11y-01-vscode-accessibility-reference.mp3 -5f1a86b1-0325-437f-bba5-2870f74ba30c,77,ep,ep45-vscode-accessibility-deep-dive.mp3,a11y-02-vscode-accessibility-deep-dive.mp3 -dfd1b7c1-6692-40b4-9085-671158a24faa,78,ep,ep49-github-accessibility-and-open-source.mp3,a11y-03-github-accessibility-and-open-source.mp3 -8dc5a501-50d6-4ece-bd97-a39bad8e7345,79,ep,ep56-document-developer-tools.mp3,a11y-04-document-developer-tools.mp3 diff --git a/tmp-proposed-stage-1-1.csv b/tmp-proposed-stage-1-1.csv deleted file mode 100644 index 612b81c1..00000000 --- a/tmp-proposed-stage-1-1.csv +++ /dev/null @@ -1,80 +0,0 @@ -uuid,current_slug,narration_id,filename,topic_prefix,topic_number,learning_band,title -5782d363-05ad-4015-a53b-69311bf4a024,ep00-welcome,ep00,ch-00-welcome.mp3,ch,00,chapter,Chapter 00: Welcome to Git Going with GitHub -e7b269f2-4c6b-4644-bfce-0aaf2f8937fe,ep01-pre-workshop-setup,ep01,ch-01-pre-workshop-setup.mp3,ch,01,chapter,Chapter 01: Pre-Workshop Setup -941a3a2b-554f-41dc-96b9-5b98443054b1,ep02-github-web-structure,ep02,ch-02-github-web-structure.mp3,ch,02,chapter,Chapter 02: Understanding GitHub on the Web -a3fc92a0-46cb-4be3-9a61-74ebc8a0b429,ep03-navigating-repositories,ep03,ch-03-navigating-repositories.mp3,ch,03,chapter,Chapter 03: Navigating Repositories -a4c497d9-535e-4040-a3e4-bfeffb9401ae,ep04-the-learning-room,ep04,ch-04-the-learning-room.mp3,ch,04,chapter,Chapter 04: The Learning Room -e2c04356-2759-42bf-ac4a-3f1326db1213,ep05-working-with-issues,ep05,ch-05-working-with-issues.mp3,ch,05,chapter,Chapter 05: Working with Issues -6c7d19fd-bd1e-4be6-adb2-9f0b48a552c8,ep06-working-with-pull-requests,ep06,ch-06-working-with-pull-requests.mp3,ch,06,chapter,Chapter 06: Working with Pull Requests -7c1f250f-100d-40e5-b2de-5f6cb05d6e9f,ep07-merge-conflicts,ep07,ch-07-merge-conflicts.mp3,ch,07,chapter,Chapter 07: Merge Conflicts Are Not Scary -03c96ea0-7cfa-428c-bd62-c1f6b8f70318,ep08-culture-and-etiquette,ep08,ch-08-culture-and-etiquette.mp3,ch,08,chapter,Chapter 08: Open Source Culture and Etiquette -825f9c85-a86e-4b2c-a4b2-4f33de3767f3,ep09-labels-milestones-projects,ep09,ch-09-labels-milestones-projects.mp3,ch,09,chapter,"Chapter 09: Labels, Milestones, and Projects" -a9c6f562-4499-4659-8e98-dc33365bb389,ep10-notifications,ep10,ch-10-notifications.mp3,ch,10,chapter,Chapter 10: Notifications and Mentions -e20fe72e-c09c-4c1c-9c35-a3d6dd03dbb7,ep11-vscode-basics,ep11,ch-11-vscode-basics.mp3,ch,11,chapter,Chapter 11: VS Code Setup and Accessibility -f12549cd-8ed9-40b9-9e82-5f1cc2f02cfe,ep12-git-source-control,ep12,ch-12-git-source-control.mp3,ch,12,chapter,Chapter 12: Git and Source Control in VS Code -825592b8-b6e3-40b3-a62e-27a3592f92f3,ep13-github-prs-extension,ep13,ch-13-github-prs-extension.mp3,ch,13,chapter,Chapter 13: The GitHub Pull Requests Extension -97ff2e66-6125-4ec5-afae-cbbfb4529873,ep14-github-copilot,ep14,ch-14-github-copilot.mp3,ch,14,chapter,Chapter 14: GitHub Copilot -47ac569d-8fda-4ab9-848f-195728a37863,ep15-accessible-code-review,ep15,ch-15-accessible-code-review.mp3,ch,15,chapter,Chapter 15: Accessible Code Review -d0070b6b-f6b1-4c60-af6c-da79b163374b,ep16-issue-templates,ep16,ch-16-issue-templates.mp3,ch,16,chapter,Chapter 16: Issue Templates -5d406c5d-fda1-428c-9b18-63a2c37c12bf,ep17-accessibility-agents,ep17,ch-17-accessibility-agents.mp3,ch,17,chapter,Chapter 17: Accessibility Agents -c4cb6bdb-7999-4d4a-87cc-3855192ea413,ep18-glossary,ep18,ref-01-glossary.mp3,ref,01,ref,Reference 01: Glossary of Terms -a0edc294-39b9-4c4b-8b88-0184fcb61941,ep19-screen-reader-cheatsheet,ep19,ref-02-screen-reader-cheatsheet.mp3,ref,02,ref,Reference 02: Screen Reader Cheat Sheet -efb32923-7fb1-4c3a-9d8f-c1c5bf2caae1,ep20-accessibility-standards,ep20,ref-03-accessibility-standards.mp3,ref,03,ref,Reference 03: Accessibility Standards Reference -de290bca-ca2a-4303-8cb9-84c0ee7abf9e,ep21-git-authentication,ep21,git-01-git-authentication.mp3,git,01,git,Git Toolkit 01: Git Authentication -ec21e605-c5ca-4ea5-93dc-44d9c3f3dead,ep22-github-flavored-markdown,ep22,tools-01-github-flavored-markdown.mp3,tools,01,tools,Tooling 01: GitHub Flavored Markdown -e4850faf-13b1-43b2-9d66-cc82740efc34,ep23-github-gists,ep23,tools-02-github-gists.mp3,tools,02,tools,Tooling 02: GitHub Gists -af3521b2-0271-4dc0-b725-aed7771fe0ae,ep24-github-discussions,ep24,tools-03-github-discussions.mp3,tools,03,tools,Tooling 03: GitHub Discussions -faa0a825-d870-43df-97e8-b4b921f95aff,ep25-releases-tags-insights,ep25,tools-04-releases-tags-insights.mp3,tools,04,tools,"Tooling 04: Releases, Tags, and Insights" -5cc23d77-6061-4092-8d00-cd10d90b631d,ep26-github-projects,ep26,tools-05-github-projects.mp3,tools,05,tools,Tooling 05: GitHub Projects Deep Dive -df063762-71bf-42e5-9fe2-c4e45a942ac1,ep27-advanced-search,ep27,tools-06-advanced-search.mp3,tools,06,tools,Tooling 06: Advanced Search -a3f97849-1fa6-4a48-b0b8-232002bcaf0c,ep28-branch-protection,ep28,sec-01-branch-protection.mp3,sec,01,sec,Security 01: Branch Protection and Rulesets -bd10a748-164c-48a5-a02c-d75be1243a81,ep29-security-features,ep29,sec-02-security-features.mp3,sec,02,sec,Security 02: GitHub Security Features -a8f94597-272c-45c9-982c-b06cd9adc54a,ep30-vscode-accessibility-reference,ep30,a11y-01-vscode-accessibility-reference.mp3,a11y,01,a11y,Accessibility 01: VS Code Accessibility Reference -1134a33e-60ba-4192-a256-952384b39e1d,ep31-github-codespaces,ep31,tools-07-github-codespaces.mp3,tools,07,tools,Tooling 07: GitHub Codespaces -c337d0ac-3e0e-41db-a12e-898919b8e08e,ep32-github-mobile,ep32,tools-08-github-mobile.mp3,tools,08,tools,Tooling 08: GitHub Mobile -3d23be9d-f3a3-479e-9814-e494a11e06f9,ep33-github-pages,ep33,tools-09-github-pages.mp3,tools,09,tools,Tooling 09: Publishing with GitHub Pages -83f42282-5dd3-4155-9809-a309af1566f8,ep34-github-actions,ep34,tools-10-github-actions.mp3,tools,10,tools,Tooling 10: GitHub Actions and Workflows -afe0af59-e122-4946-b257-83820bb772f6,ep35-profile-sponsors-wikis,ep35,tools-11-profile-sponsors-wikis.mp3,tools,11,tools,"Tooling 11: Profile, Sponsors, and Wikis" -f9b69058-5a1f-4550-8540-68310a5a2bbd,ep36-organizations-templates,ep36,tools-12-organizations-templates.mp3,tools,12,tools,Tooling 12: Organizations and Templates -34ba0bf7-f732-4eff-a700-610f2d640003,ep37-contributing-to-open-source,ep37,ch-21-contributing-to-open-source.mp3,ch,21,chapter,Chapter 21: Contributing to Open Source -fdb46e34-3dfe-4109-9374-7c68340163e4,ep38-resources,ep38,ref-04-resources.mp3,ref,04,ref,Reference 04: Resources and Links -3ba6d2d5-808a-4f31-a8ac-088f38f5655b,ep39-accessibility-agents-reference,ep39,agents-01-accessibility-agents-reference.mp3,agents,01,agents,Agents 01: Accessibility Agents - Complete Reference -60e9e20d-59dc-4618-b21f-eba8a2f087e7,ep40-copilot-reference,ep40,agents-02-copilot-reference.mp3,agents,02,agents,Agents 02: Copilot Reference -bd284e08-3097-4b46-a237-c586cf916d3e,ep41-copilot-models,ep41,agents-03-copilot-models.mp3,agents,03,agents,Agents 03: Copilot Billing and Models -f6c09e20-76ed-4107-8317-e785fc400187,ep42-accessing-workshop-materials,ep42,ref-05-accessing-workshop-materials.mp3,ref,05,ref,Reference 05: Accessing Workshop Materials -56c7b915-f209-4468-9be1-501ffbe76fc8,ep43-github-skills-catalog,ep43,ref-06-github-skills-catalog.mp3,ref,06,ref,Reference 06: GitHub Skills - Complete Course Catalog -9db086ca-c6c0-476d-bfdb-74fb16ed4970,ep44-choose-your-tools,ep44,agents-04-choose-your-tools.mp3,agents,04,agents,Agents 04: Choose Your Tools -5f1a86b1-0325-437f-bba5-2870f74ba30c,ep45-vscode-accessibility-deep-dive,ep45,a11y-02-vscode-accessibility-deep-dive.mp3,a11y,02,a11y,Accessibility 02: VS Code Accessibility Deep Dive -ae6eeda0-18af-4915-8c10-1b7ec69b212c,ep46-how-git-works,ep46,ch-18-how-git-works.mp3,ch,18,chapter,Chapter 18: How Git Works: The Mental Model -3cb0f067-09b5-4328-8613-734b5294e428,ep47-fork-and-contribute,ep47,ch-19-fork-and-contribute.mp3,ch,19,chapter,Chapter 19: Fork and Contribute -917558eb-d37b-46e8-af5d-d1f6e3816cc1,ep48-build-your-agent-capstone,ep48,ch-20-build-your-agent-capstone.mp3,ch,20,chapter,Chapter 20: Build Your Agent: Capstone -dfd1b7c1-6692-40b4-9085-671158a24faa,ep49-github-accessibility-and-open-source,ep49,a11y-03-github-accessibility-and-open-source.mp3,a11y,03,a11y,Accessibility 03: GitHub Accessibility and Open Source at Scale -3d60a94f-0060-4077-86d7-7378059ec042,ep50-advanced-git-operations,ep50,git-02-advanced-git-operations.mp3,git,02,git,Git Toolkit 02: Advanced Git Operations -adecbc6a-1d8d-41e1-ab51-e69722edfa3f,ep51-git-security-for-contributors,ep51,git-03-git-security-for-contributors.mp3,git,03,git,Git Toolkit 03: Git Security for Contributors -63b03ed9-a69d-49cf-a285-bd34235899b1,ep52-github-desktop,ep52,git-04-github-desktop.mp3,git,04,git,Git Toolkit 04: GitHub Desktop -c1bbc918-7686-4959-84e5-b70bc4b75b42,ep53-github-cli-reference,ep53,git-05-github-cli-reference.mp3,git,05,git,Git Toolkit 05: GitHub CLI Reference -54161b03-64e0-4aec-867b-360ce3846a3a,ep54-agent-installation-setup,ep54,agents-05-agent-installation-setup.mp3,agents,05,agents,Agents 05: Agent Installation and Setup -19d7104a-29aa-4ec0-8d16-c03735d76e86,ep55-advanced-agent-patterns,ep55,agents-06-advanced-agent-patterns.mp3,agents,06,agents,Agents 06: Advanced Agent Patterns -8dc5a501-50d6-4ece-bd97-a39bad8e7345,ep56-document-developer-tools,ep56,a11y-04-document-developer-tools.mp3,a11y,04,a11y,Accessibility 04: Document Developer Tools -8e96a939-869f-406c-bf9f-196677343056,ep79-what-comes-next,ep79,ch-22-what-comes-next.mp3,ch,22,chapter,Chapter 22: What Comes Next -106673ef-0050-43cb-987a-fedc06f6c2e6,cc-01-find-your-way-around,cc-01,cc-01-find-your-way-around.mp3,cc,01,challenge,Challenge 01: Find Your Way Around -0a76b55e-ee02-4587-949c-5966f7fba088,cc-02-file-your-first-issue,cc-02,cc-02-file-your-first-issue.mp3,cc,02,challenge,Challenge 02: File Your First Issue -02a68f58-dc8c-4ba0-94b4-116b9900b895,cc-03-join-the-conversation,cc-03,cc-03-join-the-conversation.mp3,cc,03,challenge,Challenge 03: Join the Conversation -5cc815f1-a897-4478-8397-c55c0833752b,cc-04-branch-out,cc-04,cc-04-branch-out.mp3,cc,04,challenge,Challenge 04: Branch Out -526854a2-83e3-42c3-8222-567718ceb41c,cc-05-make-your-mark,cc-05,cc-05-make-your-mark.mp3,cc,05,challenge,Challenge 05: Make Your Mark -4345dad5-18a5-4f95-870b-54c17faf14be,cc-06-open-your-first-pr,cc-06,cc-06-open-your-first-pr.mp3,cc,06,challenge,Challenge 06: Open Your First Pull Request -67e93c8d-b94b-4dbf-83c5-52179e85f27b,cc-07-survive-a-merge-conflict,cc-07,cc-07-survive-a-merge-conflict.mp3,cc,07,challenge,Challenge 07: Survive a Merge Conflict -023e61f1-6942-488b-8a66-47ce4570fecd,cc-08-open-source-culture,cc-08,cc-08-open-source-culture.mp3,cc,08,challenge,Challenge 08: The Culture Layer -79d813eb-5c80-4005-8c88-c9223d73f042,cc-09-merge-day,cc-09,cc-09-merge-day.mp3,cc,09,challenge,Challenge 09: Merge Day -5907cab6-ce2e-4b08-9495-18648802e46d,cc-10-go-local,cc-10,cc-10-go-local.mp3,cc,10,challenge,Challenge 10: Go Local -5dedb746-27de-4565-b071-5027e25febc1,cc-11-day-2-pull-request,cc-11,cc-11-day-2-pull-request.mp3,cc,11,challenge,Challenge 11: Open a Day 2 PR -d1e57b45-7db2-4b14-8b42-f438952d1af4,cc-12-code-review,cc-12,cc-12-code-review.mp3,cc,12,challenge,Challenge 12: Review Like a Pro -23a8abfb-78b5-44c9-ab27-75afdc88ed62,cc-13-copilot-as-collaborator,cc-13,cc-13-copilot-as-collaborator.mp3,cc,13,challenge,Challenge 13: AI as Your Copilot -58d59cbe-fc5e-412d-9b38-45d004446483,cc-14-design-an-issue-template,cc-14,cc-14-design-an-issue-template.mp3,cc,14,challenge,Challenge 14: Template Remix -518ae03c-a223-43f8-8171-14a4e528682a,cc-15-discover-accessibility-agents,cc-15,cc-15-discover-accessibility-agents.mp3,cc,15,challenge,Challenge 15: Meet the Agents -18a3b135-6150-4e59-8e01-bd5930113bc1,cc-16-build-your-own-agent,cc-16,cc-16-build-your-own-agent.mp3,cc,16,challenge,Challenge 16: Build Your Agent (Capstone) -d57260b5-bd03-4cee-8c29-72717c4389da,cc-bonus-a-improve-agent,cc-bonus-a,cc-bonus-a-improve-agent.mp3,cc-bonus,a,bonus,Bonus A: Improve an Agent -a99aec07-b2b5-4921-aece-56d83f99869e,cc-bonus-b-document-your-journey,cc-bonus-b,cc-bonus-b-document-your-journey.mp3,cc-bonus,b,bonus,Bonus B: Document Your Journey -d21b850a-6288-43a5-8271-2777e3c082d3,cc-bonus-c-group-challenge,cc-bonus-c,cc-bonus-c-group-challenge.mp3,cc-bonus,c,bonus,Bonus C: Group Challenge -77b3f85a-361f-4abe-9960-f84c30787e48,cc-bonus-d-notifications,cc-bonus-d,cc-bonus-d-notifications.mp3,cc-bonus,d,bonus,Bonus D: Notifications -77900cf3-9dbe-4739-b7fa-1cb690b9d6c1,cc-bonus-e-git-history,cc-bonus-e,cc-bonus-e-git-history.mp3,cc-bonus,e,bonus,Bonus E: Git History diff --git a/tmp-rename-audit.log b/tmp-rename-audit.log deleted file mode 100644 index 905fd69a..00000000 --- a/tmp-rename-audit.log +++ /dev/null @@ -1,119 +0,0 @@ -# Stage 3.2 audio rename log -# audio_dir: C:\code\git-going-with-github\podcasts\audio\kokoro-am_liam-af_jessica -RENAME ep00-welcome.mp3 ch-00-welcome.mp3 5782d363-05ad-4015-a53b-69311bf4a024 -RENAME ep01-pre-workshop-setup.mp3 ch-01-pre-workshop-setup.mp3 e7b269f2-4c6b-4644-bfce-0aaf2f8937fe -RENAME ep02-github-web-structure.mp3 ch-02-github-web-structure.mp3 941a3a2b-554f-41dc-96b9-5b98443054b1 -RENAME ep03-navigating-repositories.mp3 ch-03-navigating-repositories.mp3 a3fc92a0-46cb-4be3-9a61-74ebc8a0b429 -RENAME ep04-the-learning-room.mp3 ch-04-the-learning-room.mp3 a4c497d9-535e-4040-a3e4-bfeffb9401ae -RENAME ep05-working-with-issues.mp3 ch-05-working-with-issues.mp3 e2c04356-2759-42bf-ac4a-3f1326db1213 -RENAME ep06-working-with-pull-requests.mp3 ch-06-working-with-pull-requests.mp3 6c7d19fd-bd1e-4be6-adb2-9f0b48a552c8 -RENAME ep07-merge-conflicts.mp3 ch-07-merge-conflicts.mp3 7c1f250f-100d-40e5-b2de-5f6cb05d6e9f -RENAME ep08-culture-and-etiquette.mp3 ch-08-culture-and-etiquette.mp3 03c96ea0-7cfa-428c-bd62-c1f6b8f70318 -RENAME ep09-labels-milestones-projects.mp3 ch-09-labels-milestones-projects.mp3 825f9c85-a86e-4b2c-a4b2-4f33de3767f3 -RENAME ep10-notifications.mp3 ch-10-notifications.mp3 a9c6f562-4499-4659-8e98-dc33365bb389 -RENAME ep11-vscode-basics.mp3 ch-11-vscode-basics.mp3 e20fe72e-c09c-4c1c-9c35-a3d6dd03dbb7 -RENAME ep12-git-source-control.mp3 ch-12-git-source-control.mp3 f12549cd-8ed9-40b9-9e82-5f1cc2f02cfe -RENAME ep13-github-prs-extension.mp3 ch-13-github-prs-extension.mp3 825592b8-b6e3-40b3-a62e-27a3592f92f3 -RENAME ep14-github-copilot.mp3 ch-14-github-copilot.mp3 97ff2e66-6125-4ec5-afae-cbbfb4529873 -RENAME ep15-accessible-code-review.mp3 ch-15-accessible-code-review.mp3 47ac569d-8fda-4ab9-848f-195728a37863 -RENAME ep16-issue-templates.mp3 ch-16-issue-templates.mp3 d0070b6b-f6b1-4c60-af6c-da79b163374b -RENAME ep17-accessibility-agents.mp3 ch-17-accessibility-agents.mp3 5d406c5d-fda1-428c-9b18-63a2c37c12bf -RENAME ep18-glossary.mp3 ref-01-glossary.mp3 c4cb6bdb-7999-4d4a-87cc-3855192ea413 -RENAME ep19-screen-reader-cheatsheet.mp3 ref-02-screen-reader-cheatsheet.mp3 a0edc294-39b9-4c4b-8b88-0184fcb61941 -RENAME ep20-accessibility-standards.mp3 ref-03-accessibility-standards.mp3 efb32923-7fb1-4c3a-9d8f-c1c5bf2caae1 -RENAME ep21-git-authentication.mp3 git-01-git-authentication.mp3 de290bca-ca2a-4303-8cb9-84c0ee7abf9e -RENAME ep22-github-flavored-markdown.mp3 tools-01-github-flavored-markdown.mp3 ec21e605-c5ca-4ea5-93dc-44d9c3f3dead -RENAME ep23-github-gists.mp3 tools-02-github-gists.mp3 e4850faf-13b1-43b2-9d66-cc82740efc34 -RENAME ep24-github-discussions.mp3 tools-03-github-discussions.mp3 af3521b2-0271-4dc0-b725-aed7771fe0ae -RENAME ep25-releases-tags-insights.mp3 tools-04-releases-tags-insights.mp3 faa0a825-d870-43df-97e8-b4b921f95aff -RENAME ep26-github-projects.mp3 tools-05-github-projects.mp3 5cc23d77-6061-4092-8d00-cd10d90b631d -RENAME ep27-advanced-search.mp3 tools-06-advanced-search.mp3 df063762-71bf-42e5-9fe2-c4e45a942ac1 -RENAME ep28-branch-protection.mp3 sec-01-branch-protection.mp3 a3f97849-1fa6-4a48-b0b8-232002bcaf0c -RENAME ep29-security-features.mp3 sec-02-security-features.mp3 bd10a748-164c-48a5-a02c-d75be1243a81 -RENAME ep30-vscode-accessibility-reference.mp3 a11y-01-vscode-accessibility-reference.mp3 a8f94597-272c-45c9-982c-b06cd9adc54a -RENAME ep31-github-codespaces.mp3 tools-07-github-codespaces.mp3 1134a33e-60ba-4192-a256-952384b39e1d -RENAME ep32-github-mobile.mp3 tools-08-github-mobile.mp3 c337d0ac-3e0e-41db-a12e-898919b8e08e -RENAME ep33-github-pages.mp3 tools-09-github-pages.mp3 3d23be9d-f3a3-479e-9814-e494a11e06f9 -RENAME ep34-github-actions.mp3 tools-10-github-actions.mp3 83f42282-5dd3-4155-9809-a309af1566f8 -RENAME ep35-profile-sponsors-wikis.mp3 tools-11-profile-sponsors-wikis.mp3 afe0af59-e122-4946-b257-83820bb772f6 -RENAME ep36-organizations-templates.mp3 tools-12-organizations-templates.mp3 f9b69058-5a1f-4550-8540-68310a5a2bbd -RENAME ep37-contributing-to-open-source.mp3 ch-21-contributing-to-open-source.mp3 34ba0bf7-f732-4eff-a700-610f2d640003 -RENAME ep38-resources.mp3 ref-04-resources.mp3 fdb46e34-3dfe-4109-9374-7c68340163e4 -RENAME ep39-accessibility-agents-reference.mp3 agents-01-accessibility-agents-reference.mp3 3ba6d2d5-808a-4f31-a8ac-088f38f5655b -RENAME ep40-copilot-reference.mp3 agents-02-copilot-reference.mp3 60e9e20d-59dc-4618-b21f-eba8a2f087e7 -RENAME ep41-copilot-models.mp3 agents-03-copilot-models.mp3 bd284e08-3097-4b46-a237-c586cf916d3e -RENAME ep42-accessing-workshop-materials.mp3 ref-05-accessing-workshop-materials.mp3 f6c09e20-76ed-4107-8317-e785fc400187 -RENAME ep43-github-skills-catalog.mp3 ref-06-github-skills-catalog.mp3 56c7b915-f209-4468-9be1-501ffbe76fc8 -RENAME ep44-choose-your-tools.mp3 agents-04-choose-your-tools.mp3 9db086ca-c6c0-476d-bfdb-74fb16ed4970 -RENAME ep45-vscode-accessibility-deep-dive.mp3 a11y-02-vscode-accessibility-deep-dive.mp3 5f1a86b1-0325-437f-bba5-2870f74ba30c -RENAME ep46-how-git-works.mp3 ch-18-how-git-works.mp3 ae6eeda0-18af-4915-8c10-1b7ec69b212c -RENAME ep47-fork-and-contribute.mp3 ch-19-fork-and-contribute.mp3 3cb0f067-09b5-4328-8613-734b5294e428 -RENAME ep48-build-your-agent-capstone.mp3 ch-20-build-your-agent-capstone.mp3 917558eb-d37b-46e8-af5d-d1f6e3816cc1 -RENAME ep49-github-accessibility-and-open-source.mp3 a11y-03-github-accessibility-and-open-source.mp3 dfd1b7c1-6692-40b4-9085-671158a24faa -RENAME ep50-advanced-git-operations.mp3 git-02-advanced-git-operations.mp3 3d60a94f-0060-4077-86d7-7378059ec042 -RENAME ep51-git-security-for-contributors.mp3 git-03-git-security-for-contributors.mp3 adecbc6a-1d8d-41e1-ab51-e69722edfa3f -RENAME ep52-github-desktop.mp3 git-04-github-desktop.mp3 63b03ed9-a69d-49cf-a285-bd34235899b1 -RENAME ep53-github-cli-reference.mp3 git-05-github-cli-reference.mp3 c1bbc918-7686-4959-84e5-b70bc4b75b42 -RENAME ep54-agent-installation-setup.mp3 agents-05-agent-installation-setup.mp3 54161b03-64e0-4aec-867b-360ce3846a3a -RENAME ep55-advanced-agent-patterns.mp3 agents-06-advanced-agent-patterns.mp3 19d7104a-29aa-4ec0-8d16-c03735d76e86 -RENAME ep56-document-developer-tools.mp3 a11y-04-document-developer-tools.mp3 8dc5a501-50d6-4ece-bd97-a39bad8e7345 -RENAME ep79-what-comes-next.mp3 ch-22-what-comes-next.mp3 8e96a939-869f-406c-bf9f-196677343056 -# chapter JSON renames -CHAPTER ep00-welcome.json ch-00-welcome.json -CHAPTER ep01-pre-workshop-setup.json ch-01-pre-workshop-setup.json -CHAPTER ep02-github-web-structure.json ch-02-github-web-structure.json -CHAPTER ep03-navigating-repositories.json ch-03-navigating-repositories.json -CHAPTER ep04-the-learning-room.json ch-04-the-learning-room.json -CHAPTER ep05-working-with-issues.json ch-05-working-with-issues.json -CHAPTER ep06-working-with-pull-requests.json ch-06-working-with-pull-requests.json -CHAPTER ep07-merge-conflicts.json ch-07-merge-conflicts.json -CHAPTER ep08-culture-and-etiquette.json ch-08-culture-and-etiquette.json -CHAPTER ep09-labels-milestones-projects.json ch-09-labels-milestones-projects.json -CHAPTER ep10-notifications.json ch-10-notifications.json -CHAPTER ep11-vscode-basics.json ch-11-vscode-basics.json -CHAPTER ep12-git-source-control.json ch-12-git-source-control.json -CHAPTER ep13-github-prs-extension.json ch-13-github-prs-extension.json -CHAPTER ep14-github-copilot.json ch-14-github-copilot.json -CHAPTER ep15-accessible-code-review.json ch-15-accessible-code-review.json -CHAPTER ep16-issue-templates.json ch-16-issue-templates.json -CHAPTER ep17-accessibility-agents.json ch-17-accessibility-agents.json -CHAPTER ep18-glossary.json ref-01-glossary.json -CHAPTER ep19-screen-reader-cheatsheet.json ref-02-screen-reader-cheatsheet.json -CHAPTER ep20-accessibility-standards.json ref-03-accessibility-standards.json -CHAPTER ep21-git-authentication.json git-01-git-authentication.json -CHAPTER ep22-github-flavored-markdown.json tools-01-github-flavored-markdown.json -CHAPTER ep23-github-gists.json tools-02-github-gists.json -CHAPTER ep24-github-discussions.json tools-03-github-discussions.json -CHAPTER ep25-releases-tags-insights.json tools-04-releases-tags-insights.json -CHAPTER ep26-github-projects.json tools-05-github-projects.json -CHAPTER ep27-advanced-search.json tools-06-advanced-search.json -CHAPTER ep28-branch-protection.json sec-01-branch-protection.json -CHAPTER ep29-security-features.json sec-02-security-features.json -CHAPTER ep30-vscode-accessibility-reference.json a11y-01-vscode-accessibility-reference.json -CHAPTER ep31-github-codespaces.json tools-07-github-codespaces.json -CHAPTER ep32-github-mobile.json tools-08-github-mobile.json -CHAPTER ep33-github-pages.json tools-09-github-pages.json -CHAPTER ep34-github-actions.json tools-10-github-actions.json -CHAPTER ep35-profile-sponsors-wikis.json tools-11-profile-sponsors-wikis.json -CHAPTER ep36-organizations-templates.json tools-12-organizations-templates.json -CHAPTER ep37-contributing-to-open-source.json ch-21-contributing-to-open-source.json -CHAPTER ep38-resources.json ref-04-resources.json -CHAPTER ep39-accessibility-agents-reference.json agents-01-accessibility-agents-reference.json -CHAPTER ep40-copilot-reference.json agents-02-copilot-reference.json -CHAPTER ep41-copilot-models.json agents-03-copilot-models.json -CHAPTER ep42-accessing-workshop-materials.json ref-05-accessing-workshop-materials.json -CHAPTER ep43-github-skills-catalog.json ref-06-github-skills-catalog.json -CHAPTER ep44-choose-your-tools.json agents-04-choose-your-tools.json -CHAPTER ep45-vscode-accessibility-deep-dive.json a11y-02-vscode-accessibility-deep-dive.json -CHAPTER ep46-how-git-works.json ch-18-how-git-works.json -CHAPTER ep47-fork-and-contribute.json ch-19-fork-and-contribute.json -CHAPTER ep48-build-your-agent-capstone.json ch-20-build-your-agent-capstone.json -CHAPTER ep49-github-accessibility-and-open-source.json a11y-03-github-accessibility-and-open-source.json -CHAPTER ep50-advanced-git-operations.json git-02-advanced-git-operations.json -CHAPTER ep51-git-security-for-contributors.json git-03-git-security-for-contributors.json -CHAPTER ep52-github-desktop.json git-04-github-desktop.json -CHAPTER ep53-github-cli-reference.json git-05-github-cli-reference.json -CHAPTER ep54-agent-installation-setup.json agents-05-agent-installation-setup.json -CHAPTER ep55-advanced-agent-patterns.json agents-06-advanced-agent-patterns.json -CHAPTER ep56-document-developer-tools.json a11y-04-document-developer-tools.json -CHAPTER ep79-what-comes-next.json ch-22-what-comes-next.json

    PROVISIONING_APP_INSTALLATION_IDThe Installation ID from Phase 2Optional. The Installation ID from Phase 2; when unset, provisioning discovers it from the App installations
    PROVISIONING_APP_PRIVATE_KEY